[Mac] How to handle Dark Mode toggle?

Mac OSX specific forum
User avatar
kenmo
Addict
Addict
Posts: 1967
Joined: Tue Dec 23, 2003 3:54 am

[Mac] How to handle Dark Mode toggle?

Post by kenmo »

Hello, I'm using PB 5.73 x64 on MacOS Catalina and it *mostly* handles Light Mode vs Dark Mode correctly.

Based on some forum code and PB IDE code, I have workarounds to detect Dark Mode, and get MacOS system colors.
(I should check the authors and thank them here!)
System colors are used for drawing custom gadgets, eg. CanvasGadgets.

If I start in Light Mode --> everything looks OK
If I start in Dark Mode --> everything looks OK

But if I toggle Dark/Light Mode while my PB programs are running...
(1) Window background colors update but StatusBar background colors don't
I posted about that here: viewtopic.php?f=24&t=76728&p=565922#p565922

(2) My procedure to get the system colors still returns the initial colors... it doesn't get the updated colors

(3) (Least important) I am polling to detect when Dark Mode toggles, which is OK. But an event-driven detection would be appreciated!

Any advice? Thanks to all you Cocoa experts!



Demo:

Code: Select all

CompilerIf #PB_Compiler_OS <> #PB_OS_MacOS
  CompilerError "Please test on MacOS!"
CompilerEndIf

OpenWindow(0, 0, 0, 640, 480, "Dark Mode Test", #PB_Window_ScreenCentered | #PB_Window_SystemMenu)
CreateStatusBar(0, WindowID(0))
AddStatusBarField(#PB_Ignore)
StatusBarText(0, 0, "Status Bar Text")

Macro SetCanvasColor(Canvas, Color)
  If StartDrawing(CanvasOutput(Canvas))
    Box(0, 0, OutputWidth(), OutputHeight(), Color)
    StopDrawing()
  EndIf
EndMacro

y = 40
TextGadget(#PB_Any, 20, y, 200, 30, "windowBackgroundColor")
  CanvasGadget(0, 220, y, 30, 30, #PB_Canvas_Border)
  y + 30
TextGadget(#PB_Any, 20, y, 200, 30, "systemGrayColor")
  CanvasGadget(1, 220, y, 30, 30, #PB_Canvas_Border)
  y + 30
TextGadget(#PB_Any, 20, y, 200, 30, "controlBackgroundColor")
  CanvasGadget(2, 220, y, 30, 30, #PB_Canvas_Border)
  y + 30
TextGadget(#PB_Any, 20, y, 200, 30, "textColor")
  CanvasGadget(3, 220, y, 30, 30, #PB_Canvas_Border)
  y + 30

TextGadget(#PB_Any, 20, 420, 400, 30, "Toggle Dark Mode, and see if the Status Bar below changes color")

AddWindowTimer(0, 0, 1000)

DarkModeFlag.i = -1



Procedure.i Cocoa_IsDarkMode()
  Protected *appearance = CocoaMessage(0, CocoaMessage(0, 0, "NSUserDefaults standardUserDefaults"), "stringForKey:$", @"AppleInterfaceStyle")
  If (*appearance)
    *appearance = CocoaMessage(0, *appearance, "UTF8String")
    If (FindString(PeekS(*appearance, -1, #PB_UTF8), "Dark"))
      ProcedureReturn (#True)
    EndIf
  EndIf
  ProcedureReturn (#False)
EndProcedure


Procedure.i Cocoa_GetSysColor(NSColorName.s)
  ; "windowBackgroundColor"
  ; "systemGrayColor"
  ; "controlBackgroundColor"
  ; "textColor"
  
  Protected.CGFloat r, g, b
  Protected NSColor.i, NSColorSpace.i
  
  ; There is no controlAccentColor on macOS < 10.14
  If ((NSColorName = "controlAccentColor") And (OSVersion() < #PB_OS_MacOSX_10_14))
    ProcedureReturn ($D5ABAD)
  EndIf
  
  ; There are no system colors on macOS < 10.10
  If ((Left(NSColorName, 6) = "system") And (OSVersion() < #PB_OS_MacOSX_10_10))
    NSColorName = LCase(Mid(NSColorName, 7, 1)) + Mid(NSColorName, 8)
  EndIf
  
  NSColorSpace = CocoaMessage(0, 0, "NSColorSpace deviceRGBColorSpace")
  NSColor = CocoaMessage(0, CocoaMessage(0, 0, "NSColor " + NSColorName), "colorUsingColorSpace:", NSColorSpace)
  If (NSColor)
    CocoaMessage(@r, NSColor, "redComponent")
    CocoaMessage(@g, NSColor, "greenComponent")
    CocoaMessage(@b, NSColor, "blueComponent")
    ProcedureReturn (RGB(r * 255.0, g * 255.0, b * 255.0))
  EndIf
EndProcedure



Repeat
  Event = WaitWindowEvent()
  If (Event = #PB_Event_Timer)
    NewDarkMode = Cocoa_IsDarkMode()
    If NewDarkMode <> DarkModeFlag
      DarkModeFlag = NewDarkMode
      SetCanvasColor(0, Cocoa_GetSysColor("windowBackgroundColor"))
      SetCanvasColor(1, Cocoa_GetSysColor("systemGrayColor"))
      SetCanvasColor(2, Cocoa_GetSysColor("controlBackgroundColor"))
      SetCanvasColor(3, Cocoa_GetSysColor("textColor"))
      If (#True) ; Even if you re-create StatusBar, it remains in original color
        FreeStatusBar(0)
        CreateStatusBar(0, WindowID(0))
        AddStatusBarField(#PB_Ignore)
        StatusBarText(0, 0, "Status Bar Text")
      EndIf
      If DarkModeFlag
        MessageRequester("", "Dark Mode!")
      Else
        MessageRequester("", "Light / Regular Mode")
      EndIf
    EndIf
  EndIf
Until (Event = #PB_Event_CloseWindow) Or (Event = #PB_Event_Menu)
User avatar
deseven
Enthusiast
Enthusiast
Posts: 362
Joined: Wed Jan 12, 2011 3:48 pm
Location: Serbia
Contact:

Re: [Mac] How to handle Dark Mode toggle?

Post by deseven »

You need to use NSAppearance setCurrentAppearance for #1 and subscribe to theme change event for #3.
Check out those 2 examples:
viewtopic.php?p=546930#p546930
viewtopic.php?p=494505#p494505

Not sure about #2, but try setting the appearance first and maybe it will be fixed.
User avatar
mk-soft
Always Here
Always Here
Posts: 5313
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: [Mac] How to handle Dark Mode toggle?

Post by mk-soft »

To respond to the colour change, deseven has already found.

I reported the problem with the status bar as a bug.
In order to surround the problem with the status bar for the time being, I have simply replaced the library "statusbar" in PB version v5.73 with PB version v5.70. So we are waiting for an update.

With the library "statusbar" from version v5.70, the color change also works.

Code: Select all

;-TOP

Procedure NSColorByNameToRGB(NSColorName.s)
  Protected.cgfloat red, green, blue
  Protected nscolorspace, rgb
  nscolorspace = CocoaMessage(0, CocoaMessage(0, 0, "NSColor " + NSColorName), "colorUsingColorSpaceName:$", @"NSCalibratedRGBColorSpace")
  If nscolorspace
    CocoaMessage(@red, nscolorspace, "redComponent")
    CocoaMessage(@green, nscolorspace, "greenComponent")
    CocoaMessage(@blue, nscolorspace, "blueComponent")
    rgb = RGB(red * 255.0, green * 255.0, blue * 255.0)
    ProcedureReturn rgb
  EndIf
EndProcedure

; ----

; >> Key-Value observer code <<<

EnumerationBinary
  #NSKeyValueObservingOptionNew
  #NSKeyValueObservingOptionOld
EndEnumeration

Global *NSKeyValueChangeNewKey.Integer = dlsym_(#RTLD_DEFAULT, "NSKeyValueChangeNewKey")
Global *NSKeyValueChangeOldKey.Integer = dlsym_(#RTLD_DEFAULT, "NSKeyValueChangeOldKey")

; Declare the KVO callback procedure
DeclareC KVO(obj, sel, keyPath, object, change, context)

; Create Key-Value Observer class (PB_KVO)
Global KVO_Class.i = objc_allocateClassPair_(objc_getClass_("NSObject"), "PB_KVO", 0)
class_addMethod_(KVO_Class, sel_registerName_("observeValueForKeyPath:ofObject:change:context:"), @KVO(), "v@:@@@^v")
objc_registerClassPair_(KVO_Class)

; Create PB_KVO class instance (KVO)
Global KVO.i = CocoaMessage(0, 0, "PB_KVO new")

; >> End of Key-Value observer code <<<

Enumeration #PB_Event_FirstCustomValue
  #EventChangeAppearance
EndEnumeration

ProcedureC KVO(obj, sel, keyPath, object, change, context)
  Select PeekS(CocoaMessage(0, keyPath, "UTF8String"), -1, #PB_UTF8)
      
    Case "effectiveAppearance":
      CocoaMessage(0, 0, "NSAppearance setCurrentAppearance:", CocoaMessage(0, change, "objectForKey:", *NSKeyValueChangeNewKey\i))
      PostEvent(#EventChangeAppearance)
      
  EndSelect
EndProcedure

; add observer
Global NSApp.i = CocoaMessage(0, 0, "NSApplication sharedApplication")
CocoaMessage(0, NSApp, "addObserver:", KVO, "forKeyPath:$", @"effectiveAppearance", "options:", #NSKeyValueObservingOptionNew, "context:", #nil)


Global Event, text_color, control_background_color

Procedure UpdateColors()
  text_color = NSColorByNameToRGB("textColor")
  control_background_color = NSColorByNameToRGB("windowBackgroundColor")
  
  If StartDrawing(CanvasOutput(0))
    Box(0, 0, OutputWidth(), OutputHeight(), control_background_color)
    DrawingMode(#PB_2DDrawing_Transparent)
    DrawText(10, 10, "Hello World", text_color)
    StopDrawing()
  EndIf 
EndProcedure

OpenWindow(0, #PB_Ignore, #PB_Ignore, 300, 180, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

CreateStatusBar(0, WindowID(0))
AddStatusBarField(#PB_Ignore)
StatusBarText(0, 0, " StatusBar")

CreateImage(0, 64, 64, 32, $B48246)
CocoaMessage(0, WindowID(0), "setBackgroundColor:", CocoaMessage(0, 0, "NSColor colorWithPatternImage:", ImageID(0)))

CanvasGadget(0, 10, 10, WindowWidth(0) - 20, WindowHeight(0) - 20 - StatusBarHeight(0))

UpdateColors()

Repeat
  Event = WaitWindowEvent()
  If Event = #EventChangeAppearance
    UpdateColors()
  EndIf
Until Event = #PB_Event_CloseWindow

; remove observer

CocoaMessage(0, NSApp, "removeObserver:", KVO, "forKeyPath:$", @"effectiveAppearance")
CocoaMessage(0, KVO, "release")
My Projects ThreadToGUI / OOP-BaseClass / EventDesigner V3
PB v3.30 / v5.75 - OS Mac Mini OSX 10.xx - VM Window Pro / Linux Ubuntu
Downloads on my Webspace / OneDrive
User avatar
kenmo
Addict
Addict
Posts: 1967
Joined: Tue Dec 23, 2003 3:54 am

Re: [Mac] How to handle Dark Mode toggle?

Post by kenmo »

Thank you both for your advice. If I knew Cocoa better, I probably could have solved it before from the threads you linked.
:D

Now I have a Key Value Observer to intercept theme changes, instead of constantly polling. Good!

Also, calling setCurrentAppearance results in the new, updated colors being reported. Good!!

All that remains, is the StatusBar background color doesn't update. I did not try copying the 5.70 StatusBar library yet. I would prefer a Cocoa workaround... but I have no idea how to start. Thanks for reporting the bug already.


EDIT: Can confirm, that copying over the 5.70 x64 StatusBar purelibrary "fixes" the StatusBar (its background color updates when light/dark mode changes). Thanks for all your help!!!
Post Reply