Bug 1909163 - make select dropdowns more properly tabspecific, r=emilio
[gecko.git] / widget / cocoa / nsLookAndFeel.mm
blob1d169665da54302a60a568ea1663d15d316bf4d8
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/widget/ThemeChangeKind.h"
7 #include "nsLookAndFeel.h"
8 #include "nsCocoaFeatures.h"
9 #include "nsNativeThemeColors.h"
10 #include "nsStyleConsts.h"
11 #include "nsIContent.h"
12 #include "gfxFont.h"
13 #include "gfxFontConstants.h"
14 #include "gfxPlatformMac.h"
15 #include "nsCSSColorUtils.h"
16 #include "mozilla/FontPropertyTypes.h"
17 #include "mozilla/gfx/2D.h"
18 #include "mozilla/StaticPrefs_widget.h"
19 #include "mozilla/Telemetry.h"
20 #include "mozilla/widget/WidgetMessageUtils.h"
21 #include "mozilla/MacStringHelpers.h"
23 #import <Cocoa/Cocoa.h>
24 #import <Carbon/Carbon.h>
25 #import <AppKit/NSColor.h>
27 // This must be included last:
28 #include "nsObjCExceptions.h"
30 using namespace mozilla;
32 @interface MOZLookAndFeelDynamicChangeObserver : NSObject
33 + (void)startObserving;
34 @end
36 nsLookAndFeel::nsLookAndFeel() {
37   [MOZLookAndFeelDynamicChangeObserver startObserving];
40 nsLookAndFeel::~nsLookAndFeel() = default;
42 void nsLookAndFeel::EnsureInit() {
43   if (mInitialized) {
44     return;
45   }
47   NS_OBJC_BEGIN_TRY_ABORT_BLOCK
49   mInitialized = true;
50   NSWindow* window =
51       [[NSWindow alloc] initWithContentRect:NSZeroRect
52                                   styleMask:NSWindowStyleMaskTitled
53                                     backing:NSBackingStoreBuffered
54                                       defer:NO];
55   auto release = MakeScopeExit([&] { [window release]; });
57   mRtl = window.windowTitlebarLayoutDirection ==
58          NSUserInterfaceLayoutDirectionRightToLeft;
59   mTitlebarHeight = std::ceil(window.frame.size.height);
61   RecordTelemetry();
63   NS_OBJC_END_TRY_ABORT_BLOCK
66 void nsLookAndFeel::RefreshImpl() {
67   mInitialized = false;
68   nsXPLookAndFeel::RefreshImpl();
71 static nscolor GetColorFromNSColor(NSColor* aColor) {
72   NSColor* deviceColor =
73       [aColor colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace];
74   return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
75                  (unsigned int)(deviceColor.greenComponent * 255.0),
76                  (unsigned int)(deviceColor.blueComponent * 255.0),
77                  (unsigned int)(deviceColor.alphaComponent * 255.0));
80 static nscolor GetColorFromNSColorWithCustomAlpha(NSColor* aColor,
81                                                   float alpha) {
82   NSColor* deviceColor =
83       [aColor colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
84   return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
85                  (unsigned int)(deviceColor.greenComponent * 255.0),
86                  (unsigned int)(deviceColor.blueComponent * 255.0),
87                  (unsigned int)(alpha * 255.0));
90 // Turns an opaque selection color into a partially transparent selection color,
91 // which usually leads to better contrast with the text color and which should
92 // look more visually appealing in most contexts.
93 // The idea is that the text and its regular, non-selected background are
94 // usually chosen in such a way that they contrast well. Making the selection
95 // color partially transparent causes the selection color to mix with the text's
96 // regular background, so the end result will often have better contrast with
97 // the text than an arbitrary opaque selection color.
98 // The motivating example for this is the light selection color on dark web
99 // pages: White text on a light blue selection color has very bad contrast,
100 // whereas white text on dark blue (which what you get if you mix
101 // partially-transparent light blue with the black textbox background) has much
102 // better contrast.
103 static nscolor ProcessSelectionBackground(nscolor aColor, ColorScheme aScheme) {
104   if (aScheme == ColorScheme::Dark) {
105     // When we use a dark selection color, we do not change alpha because we do
106     // not use dark selection in content. The dark system color is appropriate
107     // for Firefox UI without needing to adjust its alpha.
108     return aColor;
109   }
110   uint16_t hue, sat, value;
111   uint8_t alpha;
112   nscolor resultColor = aColor;
113   NS_RGB2HSV(resultColor, hue, sat, value, alpha);
114   int factor = 2;
115   alpha = alpha / factor;
116   if (sat > 0) {
117     // The color is not a shade of grey, restore the saturation taken away by
118     // the transparency.
119     sat = mozilla::clamped(sat * factor, 0, 255);
120   } else {
121     // The color is a shade of grey, find the value that looks equivalent
122     // on a white background with the given opacity.
123     value = mozilla::clamped(255 - (255 - value) * factor, 0, 255);
124   }
125   NS_HSV2RGB(resultColor, hue, sat, value, alpha);
126   return resultColor;
129 nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
130                                        nscolor& aColor) {
131   NS_OBJC_BEGIN_TRY_ABORT_BLOCK
133   NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
135   nscolor color = 0;
136   switch (aID) {
137     case ColorID::Infobackground:
138       color = aScheme == ColorScheme::Light
139                   ? NS_RGB(0xdd, 0xdd, 0xdd)
140                   : GetColorFromNSColor(NSColor.windowBackgroundColor);
141       break;
142     case ColorID::Highlight:
143       color = ProcessSelectionBackground(
144           GetColorFromNSColor(NSColor.selectedTextBackgroundColor), aScheme);
145       break;
146     // This is used to gray out the selection when it's not focused. Used with
147     // nsISelectionController::SELECTION_DISABLED.
148     case ColorID::TextSelectDisabledBackground:
149       color = ProcessSelectionBackground(
150           GetColorFromNSColor(NSColor.secondarySelectedControlColor), aScheme);
151       break;
152     case ColorID::MozMenuhoverdisabled:
153       aColor = NS_TRANSPARENT;
154       break;
155     case ColorID::Accentcolor:
156       color = GetColorFromNSColor(NSColor.controlAccentColor);
157       break;
158     case ColorID::MozMenuhover:
159     case ColorID::Selecteditem:
160       color = GetColorFromNSColor(NSColor.selectedContentBackgroundColor);
161       if (aID == ColorID::MozMenuhover &&
162           !LookAndFeel::GetInt(IntID::PrefersReducedTransparency)) {
163         // Wash the color a little bit with semi-transparent white to match a
164         // bit closer the native NSVisualEffectSelection on menus.
165         color = NS_ComposeColors(
166             color,
167             NS_RGBA(255, 255, 255, aScheme == ColorScheme::Light ? 51 : 25));
168       }
169       break;
170     case ColorID::Accentcolortext:
171     case ColorID::MozMenuhovertext:
172     case ColorID::Selecteditemtext:
173       color = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
174       break;
175     case ColorID::IMESelectedRawTextBackground:
176     case ColorID::IMESelectedConvertedTextBackground:
177     case ColorID::IMERawInputBackground:
178     case ColorID::IMEConvertedTextBackground:
179       color = NS_TRANSPARENT;
180       break;
181     case ColorID::IMESelectedRawTextForeground:
182     case ColorID::IMESelectedConvertedTextForeground:
183     case ColorID::IMERawInputForeground:
184     case ColorID::IMEConvertedTextForeground:
185     case ColorID::Highlighttext:
186       color = NS_SAME_AS_FOREGROUND_COLOR;
187       break;
188     case ColorID::IMERawInputUnderline:
189     case ColorID::IMEConvertedTextUnderline:
190       color = NS_40PERCENT_FOREGROUND_COLOR;
191       break;
192     case ColorID::IMESelectedRawTextUnderline:
193     case ColorID::IMESelectedConvertedTextUnderline:
194       color = NS_SAME_AS_FOREGROUND_COLOR;
195       break;
197       //
198       // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
199       //
200       // It's really hard to effectively map these to the Appearance Manager
201       // properly, since they are modeled word for word after the win32 system
202       // colors and don't have any real counterparts in the Mac world. I'm sure
203       // we'll be tweaking these for years to come.
204       //
205       // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that
206       // form the defaults
207       //  if querying the Appearance Manager fails ;)
208       //
209     case ColorID::MozMacDefaultbuttontext:
210       color = NS_RGB(0xFF, 0xFF, 0xFF);
211       break;
212     case ColorID::MozSidebar:
213       color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6)
214                                             : NS_RGB(0x2d, 0x2d, 0x2d);
215       break;
216     case ColorID::MozSidebarborder:
217       // hsla(240, 5%, 5%, .1)
218       color = NS_RGBA(12, 12, 13, 26);
219       break;
220     case ColorID::MozButtonactivetext:
221       // Pre-macOS 12, pressed buttons were filled with the highlight color and
222       // the text was white. Starting with macOS 12, pressed (non-default)
223       // buttons are filled with medium gray and the text color is the same as
224       // in the non-pressed state.
225       color = nsCocoaFeatures::OnMontereyOrLater()
226                   ? GetColorFromNSColor(NSColor.controlTextColor)
227                   : NS_RGB(0xFF, 0xFF, 0xFF);
228       break;
229     case ColorID::Windowtext:
230     case ColorID::MozDialogtext:
231       color = GetColorFromNSColor(NSColor.windowFrameTextColor);
232       break;
233     case ColorID::Appworkspace:
234       color = NS_RGB(0xFF, 0xFF, 0xFF);
235       break;
236     case ColorID::Background:
237       color = NS_RGB(0x63, 0x63, 0xCE);
238       break;
239     case ColorID::Buttonface:
240     case ColorID::MozButtonhoverface:
241     case ColorID::MozButtonactiveface:
242     case ColorID::MozButtondisabledface:
243       color = GetColorFromNSColor(NSColor.controlColor);
244       if (!NS_GET_A(color)) {
245         color = GetColorFromNSColor(NSColor.controlBackgroundColor);
246       }
247       break;
248     case ColorID::Buttonhighlight:
249       color = GetColorFromNSColor(NSColor.selectedControlColor);
250       break;
251     case ColorID::Scrollbar:
252       color = GetColorFromNSColor(NSColor.scrollBarColor);
253       break;
254     case ColorID::Threedhighlight:
255       color = GetColorFromNSColor(NSColor.highlightColor);
256       break;
257     case ColorID::Buttonshadow:
258     case ColorID::Threeddarkshadow:
259       color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
260                                            : NS_RGB(0xDC, 0xDC, 0xDC);
261       break;
262     case ColorID::Threedshadow:
263       color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
264                                            : NS_RGB(0xE0, 0xE0, 0xE0);
265       break;
266     case ColorID::Threedface:
267       color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
268                                            : NS_RGB(0xF0, 0xF0, 0xF0);
269       break;
270     case ColorID::Threedlightshadow:
271     case ColorID::Buttonborder:
272     case ColorID::MozDisabledfield:
273       color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
274                                            : NS_RGB(0xDA, 0xDA, 0xDA);
275       break;
276     case ColorID::Menu:
277       // Hand-picked from Sonoma because there doesn't seem to be any
278       // appropriate menu system color.
279       color = aScheme == ColorScheme::Dark ? NS_RGB(0x36, 0x36, 0x39)
280                                            : NS_RGB(0xeb, 0xeb, 0xeb);
281       break;
282     case ColorID::Windowframe:
283       color = GetColorFromNSColor(NSColor.windowFrameColor);
284       break;
285     case ColorID::MozDialog:
286     case ColorID::Window:
287       color = GetColorFromNSColor(aScheme == ColorScheme::Light
288                                       ? NSColor.windowBackgroundColor
289                                       : NSColor.underPageBackgroundColor);
290       break;
291     case ColorID::Field:
292     case ColorID::MozCombobox:
293       color = GetColorFromNSColor(NSColor.controlBackgroundColor);
294       break;
295     case ColorID::Fieldtext:
296     case ColorID::MozComboboxtext:
297     case ColorID::Buttontext:
298     case ColorID::MozButtonhovertext:
299     case ColorID::Menutext:
300     case ColorID::Infotext:
301     case ColorID::MozCellhighlighttext:
302     case ColorID::MozSidebartext:
303       color = GetColorFromNSColor(NSColor.controlTextColor);
304       break;
305     case ColorID::MozMacFocusring:
306       color = GetColorFromNSColorWithCustomAlpha(
307           NSColor.keyboardFocusIndicatorColor, 0.48);
308       break;
309     case ColorID::MozMacDisabledtoolbartext:
310     case ColorID::Graytext:
311       color = GetColorFromNSColor(NSColor.disabledControlTextColor);
312       break;
313     case ColorID::MozCellhighlight:
314       // For inactive list selection
315       color = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
316       break;
317     case ColorID::MozColheadertext:
318     case ColorID::MozColheaderhovertext:
319     case ColorID::MozColheaderactivetext:
320       color = GetColorFromNSColor(NSColor.headerTextColor);
321       break;
322     case ColorID::MozColheaderactive:
323       color = GetColorFromNSColor(
324           NSColor.unemphasizedSelectedContentBackgroundColor);
325       break;
326     case ColorID::MozColheader:
327     case ColorID::MozColheaderhover:
328     case ColorID::MozEventreerow:
329       // Background color of even list rows.
330       color =
331           GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
332       break;
333     case ColorID::MozOddtreerow:
334       // Background color of odd list rows.
335       color =
336           GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
337       break;
338     case ColorID::MozNativehyperlinktext:
339       color = GetColorFromNSColor(NSColor.linkColor);
340       break;
341     case ColorID::MozNativevisitedhyperlinktext:
342       color = GetColorFromNSColor(NSColor.systemPurpleColor);
343       break;
344     case ColorID::MozHeaderbartext:
345     case ColorID::MozHeaderbarinactivetext:
346     case ColorID::Inactivecaptiontext:
347     case ColorID::Captiontext:
348       aColor = GetColorFromNSColor(NSColor.textColor);
349       return NS_OK;
350     case ColorID::MozHeaderbar:
351     case ColorID::MozHeaderbarinactive:
352     case ColorID::Inactivecaption:
353     case ColorID::Activecaption:
354       // This has better contrast than the stand-in colors.
355       aColor = GetColorFromNSColor(NSColor.windowBackgroundColor);
356       return NS_OK;
357     case ColorID::Marktext:
358     case ColorID::Mark:
359     case ColorID::SpellCheckerUnderline:
360     case ColorID::Activeborder:
361     case ColorID::Inactiveborder:
362     case ColorID::MozAutofillBackground:
363     case ColorID::TargetTextBackground:
364     case ColorID::TargetTextForeground:
365       aColor = GetStandinForNativeColor(aID, aScheme);
366       return NS_OK;
367     default:
368       aColor = NS_RGB(0xff, 0xff, 0xff);
369       return NS_ERROR_FAILURE;
370   }
372   aColor = color;
373   return NS_OK;
375   NS_OBJC_END_TRY_ABORT_BLOCK
378 static bool SystemWantsDarkTheme() {
379   // This returns true if the macOS system appearance is set to dark mode,
380   // false otherwise.
381   NSAppearanceName aquaOrDarkAqua =
382       [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
383         NSAppearanceNameAqua, NSAppearanceNameDarkAqua
384       ]];
385   return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
388 nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
389   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
391   nsresult res = NS_OK;
393   switch (aID) {
394     case IntID::ScrollButtonLeftMouseButtonAction:
395       aResult = 0;
396       break;
397     case IntID::ScrollButtonMiddleMouseButtonAction:
398     case IntID::ScrollButtonRightMouseButtonAction:
399       aResult = 3;
400       break;
401     case IntID::CaretBlinkTime:
402       aResult = 567;
403       break;
404     case IntID::CaretWidth:
405       aResult = 1;
406       break;
407     case IntID::SelectTextfieldsOnKeyFocus:
408       // Select textfield content when focused by kbd
409       // used by EventStateManager::sTextfieldSelectModel
410       aResult = 1;
411       break;
412     case IntID::SubmenuDelay:
413       aResult = 200;
414       break;
415     case IntID::MenusCanOverlapOSBar:
416       // xul popups are not allowed to overlap the menubar.
417       aResult = 0;
418       break;
419     case IntID::SkipNavigatingDisabledMenuItem:
420       aResult = 1;
421       break;
422     case IntID::DragThresholdX:
423     case IntID::DragThresholdY:
424       aResult = 4;
425       break;
426     case IntID::ScrollArrowStyle:
427       aResult = eScrollArrow_None;
428       break;
429     case IntID::UseOverlayScrollbars:
430     case IntID::AllowOverlayScrollbarsOverlap:
431       aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
432       break;
433     case IntID::ScrollbarDisplayOnMouseMove:
434       aResult = 0;
435       break;
436     case IntID::ScrollbarFadeBeginDelay:
437       aResult = 450;
438       break;
439     case IntID::ScrollbarFadeDuration:
440       aResult = 200;
441       break;
442     case IntID::TreeOpenDelay:
443       aResult = 1000;
444       break;
445     case IntID::TreeCloseDelay:
446       aResult = 1000;
447       break;
448     case IntID::TreeLazyScrollDelay:
449       aResult = 150;
450       break;
451     case IntID::TreeScrollDelay:
452       aResult = 100;
453       break;
454     case IntID::TreeScrollLinesMax:
455       aResult = 3;
456       break;
457     case IntID::MacBigSurTheme:
458       aResult = nsCocoaFeatures::OnBigSurOrLater();
459       break;
460     case IntID::MacRTL:
461       EnsureInit();
462       aResult = mRtl;
463       break;
464     case IntID::MacTitlebarHeight:
465       EnsureInit();
466       aResult = mTitlebarHeight;
467       break;
468     case IntID::AlertNotificationOrigin:
469       aResult = NS_ALERT_TOP;
470       break;
471     case IntID::ScrollToClick: {
472       aResult = [[NSUserDefaults standardUserDefaults]
473           boolForKey:@"AppleScrollerPagingBehavior"];
474     } break;
475     case IntID::ChosenMenuItemsShouldBlink:
476       aResult = 1;
477       break;
478     case IntID::IMERawInputUnderlineStyle:
479     case IntID::IMEConvertedTextUnderlineStyle:
480     case IntID::IMESelectedRawTextUnderlineStyle:
481     case IntID::IMESelectedConvertedTextUnderline:
482       aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
483       break;
484     case IntID::SpellCheckerUnderlineStyle:
485       aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
486       break;
487     case IntID::ScrollbarButtonAutoRepeatBehavior:
488       aResult = 0;
489       break;
490     case IntID::SwipeAnimationEnabled:
491       aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
492       break;
493     case IntID::ContextMenuOffsetVertical:
494       aResult = -6;
495       break;
496     case IntID::ContextMenuOffsetHorizontal:
497       aResult = 1;
498       break;
499     case IntID::SystemUsesDarkTheme:
500       aResult = SystemWantsDarkTheme();
501       break;
502     case IntID::PrefersReducedMotion:
503       aResult =
504           NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
505       break;
506     case IntID::PrefersReducedTransparency:
507       aResult = NSWorkspace.sharedWorkspace
508                     .accessibilityDisplayShouldReduceTransparency;
509       break;
510     case IntID::InvertedColors:
511       aResult =
512           NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
513       break;
514     case IntID::UseAccessibilityTheme:
515       aResult = NSWorkspace.sharedWorkspace
516                     .accessibilityDisplayShouldIncreaseContrast;
517       break;
518     case IntID::PanelAnimations:
519       aResult = 1;
520       break;
521     case IntID::FullKeyboardAccess:
522       aResult = NSApp.isFullKeyboardAccessEnabled;
523       break;
524     default:
525       aResult = 0;
526       res = NS_ERROR_FAILURE;
527   }
528   return res;
530   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
533 nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
534   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
536   nsresult res = NS_OK;
538   switch (aID) {
539     case FloatID::IMEUnderlineRelativeSize:
540       aResult = 2.0f;
541       break;
542     case FloatID::SpellCheckerUnderlineRelativeSize:
543       aResult = 2.0f;
544       break;
545     case FloatID::CursorScale: {
546       id uaDefaults = [[NSUserDefaults alloc]
547           initWithSuiteName:@"com.apple.universalaccess"];
548       float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
549       [uaDefaults release];
550       aResult = f > 0.0 ? f : 1.0;  // default to 1.0 if value not available
551       break;
552     }
553     default:
554       aResult = -1.0;
555       res = NS_ERROR_FAILURE;
556   }
558   return res;
560   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
563 bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
564                                   gfxFontStyle& aFontStyle) {
565   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
567   nsAutoCString name;
568   gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
569   aFontName.Append(NS_ConvertUTF8toUTF16(name));
571   return true;
573   NS_OBJC_END_TRY_BLOCK_RETURN(false);
576 void nsLookAndFeel::RecordAccessibilityTelemetry() {
577   if ([[NSWorkspace sharedWorkspace]
578           respondsToSelector:@selector
579           (accessibilityDisplayShouldInvertColors)]) {
580     bool val =
581         [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
582     Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INVERT_COLORS, val);
583   }
586 nsresult nsLookAndFeel::GetKeyboardLayoutImpl(nsACString& aLayout) {
587   TISInputSourceRef source = ::TISCopyCurrentKeyboardInputSource();
588   nsAutoString layout;
590   CFStringRef layoutName = static_cast<CFStringRef>(
591       ::TISGetInputSourceProperty(source, kTISPropertyLocalizedName));
592   CopyNSStringToXPCOMString((const NSString*)layoutName, layout);
593   aLayout.Assign(NS_ConvertUTF16toUTF8(layout));
595   ::CFRelease(source);
596   return NS_OK;
599 @implementation MOZLookAndFeelDynamicChangeObserver
601 + (void)startObserving {
602   static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
603   if (!gInstance) {
604     gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init];  // leaked
605   }
608 - (instancetype)init {
609   self = [super init];
611   [NSNotificationCenter.defaultCenter
612       addObserver:self
613          selector:@selector(colorsChanged)
614              name:NSControlTintDidChangeNotification
615            object:nil];
616   [NSNotificationCenter.defaultCenter
617       addObserver:self
618          selector:@selector(colorsChanged)
619              name:NSSystemColorsDidChangeNotification
620            object:nil];
622   [NSWorkspace.sharedWorkspace.notificationCenter
623       addObserver:self
624          selector:@selector(mediaQueriesChanged)
625              name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
626            object:nil];
628   [NSNotificationCenter.defaultCenter
629       addObserver:self
630          selector:@selector(scrollbarsChanged)
631              name:NSPreferredScrollerStyleDidChangeNotification
632            object:nil];
633   [NSDistributedNotificationCenter.defaultCenter
634              addObserver:self
635                 selector:@selector(scrollbarsChanged)
636                     name:@"AppleAquaScrollBarVariantChanged"
637                   object:nil
638       suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
639   [NSDistributedNotificationCenter.defaultCenter
640              addObserver:self
641                 selector:@selector(cachedValuesChanged)
642                     name:@"AppleNoRedisplayAppearancePreferenceChanged"
643                   object:nil
644       suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
645   [NSDistributedNotificationCenter.defaultCenter
646              addObserver:self
647                 selector:@selector(cachedValuesChanged)
648                     name:@"com.apple.KeyboardUIModeDidChange"
649                   object:nil
650       suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
652   [NSApp addObserver:self
653           forKeyPath:@"effectiveAppearance"
654              options:0
655              context:nil];
657   return self;
660 - (void)observeValueForKeyPath:(NSString*)keyPath
661                       ofObject:(id)object
662                         change:(NSDictionary<NSKeyValueChangeKey, id>*)change
663                        context:(void*)context {
664   if ([keyPath isEqualToString:@"effectiveAppearance"]) {
665     [self entireThemeChanged];
666   } else {
667     [super observeValueForKeyPath:keyPath
668                          ofObject:object
669                            change:change
670                           context:context];
671   }
674 - (void)entireThemeChanged {
675   LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
678 - (void)scrollbarsChanged {
679   LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
682 - (void)mediaQueriesChanged {
683   // Changing`Invert Colors` sends
684   // AccessibilityDisplayOptionsDidChangeNotifications. We monitor that setting
685   // via telemetry, so call into that recording method here.
686   nsLookAndFeel::RecordAccessibilityTelemetry();
687   LookAndFeel::NotifyChangedAllWindows(
688       widget::ThemeChangeKind::MediaQueriesOnly);
691 - (void)colorsChanged {
692   LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
695 - (void)cachedValuesChanged {
696   // We only need to re-cache (and broadcast) updated LookAndFeel values, so
697   // that they're up-to-date the next time they're queried. No further change
698   // handling is needed.
699   // TODO: Add a change hint for this which avoids the unnecessary media query
700   // invalidation.
701   LookAndFeel::NotifyChangedAllWindows(
702       widget::ThemeChangeKind::MediaQueriesOnly);
704 @end