Detect change between Light and Dark Mode?

Mac OSX specific forum
wombats
Enthusiast
Enthusiast
Posts: 663
Joined: Thu Dec 29, 2011 5:03 pm

Detect change between Light and Dark Mode?

Post by wombats »

Hi,

I want to update my canvas-based gadgets when the user changes their macOS Appearance mode, but in the following, the colours are the same as before when the change happens. What am I doing wrong?

Code: Select all

EnableExplicit

Global text_color, control_background_color

Declare UpdateColors()

Define app = CocoaMessage(0,0,"NSApplication sharedApplication") ; By deseven: https://www.purebasic.fr/english/viewtopic.php?p=494505#p494505
Define appDelegate = CocoaMessage(0,app,"delegate")
Define delegateClass = object_getClass_(appDelegate)
Define selector = sel_registerName_("darkModeChanged:")
Define distributedNotificationCenter = CocoaMessage(0,0,"NSDistributedNotificationCenter defaultCenter")
Procedure darkModeChanged(notification)
  UpdateColors()
EndProcedure

class_addMethod_(delegateClass,selector,@darkModeChanged(),"v@:@")
CocoaMessage(0,distributedNotificationCenter,
             "addObserver:",appDelegate,
             "selector:",selector,
             "name:$",@"AppleInterfaceThemeChangedNotification",
             "object:",#nil)

Procedure.i GetCocoaColor(ColorName.s) ; By wilbert: https://www.purebasic.fr/english/viewtopic.php?p=419571#p419571
  Protected.i Result, Rect.NSRect, Image, NSColor = CocoaMessage(#Null, #Null, "NSColor " + ColorName)
  If NSColor
    Rect\size\width = 1
    Rect\size\height = 1
    Image = CreateImage(#PB_Any, 1, 1)
    StartDrawing(ImageOutput(Image))
    CocoaMessage(#Null, NSColor, "drawSwatchInRect:@", @Rect)
    Result = Point(0, 0)
    StopDrawing()
    FreeImage(Image)
    ProcedureReturn Result
  Else
    ProcedureReturn -1
  EndIf
EndProcedure

; ---

Procedure UpdateColors()
  text_color = GetCocoaColor("textColor")
  control_background_color = GetCocoaColor("controlBackgroundColor")
  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, 150, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

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

UpdateColors()

Repeat : Until WaitWindowEvent(100) = #PB_Event_CloseWindow
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Detect change between Light and Dark Mode?

Post by wilbert »

Try if this works ...
It's based on Key-Value observing

Code: Select all

; >> 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)
  

Procedure.i GetCocoaColor(ColorName.s) ; By wilbert: https://www.purebasic.fr/english/viewtopic.php?p=419571#p419571
  Protected.i Result, Rect.NSRect, Image, NSColor = CocoaMessage(#Null, #Null, "NSColor " + ColorName)
  If NSColor
    Rect\size\width = 1
    Rect\size\height = 1
    Image = CreateImage(#PB_Any, 1, 1)
    StartDrawing(ImageOutput(Image))
    CocoaMessage(#Null, NSColor, "drawSwatchInRect:@", @Rect)
    Result = Point(0, 0)
    StopDrawing()
    FreeImage(Image)
    ProcedureReturn Result
  Else
    ProcedureReturn -1
  EndIf
EndProcedure

; ---

Global Event, text_color, control_background_color

Procedure UpdateColors()
  text_color = GetCocoaColor("textColor")
  control_background_color = GetCocoaColor("controlBackgroundColor")
  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, 150, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)

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

UpdateColors()

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

; remove observer
CocoaMessage(0, NSApp, "removeObserver:", KVO, "forKeyPath:$", @"effectiveAppearance")
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
mk-soft
Always Here
Always Here
Posts: 5335
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: Detect change between Light and Dark Mode?

Post by mk-soft »

Thanks :D :wink:
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
wombats
Enthusiast
Enthusiast
Posts: 663
Joined: Thu Dec 29, 2011 5:03 pm

Re: Detect change between Light and Dark Mode?

Post by wombats »

Thank you, wilbert! That works great. I don't know how you manage all this stuff (I'm usually at a complete loss when trying to make something work with CocoaMessage), but I greatly appreciate it.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Detect change between Light and Dark Mode?

Post by wilbert »

wombats wrote:I don't know how you manage all this stuff
Just googling and combining pieces :wink:

What the code does is observe for changes of the effectiveAppearance property of the application, set the appearance for the current thread the same and post an event.
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
mk-soft
Always Here
Always Here
Posts: 5335
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: Detect change between Light and Dark Mode?

Post by mk-soft »

I still find it difficult to implement some functions with CocoaMessage, but it always works better.

But how do you know that the Key Value Observer at Purebasic is called "PB_KV0" to connect it with your own "KVO" :?:

P.S.
The NSObject "PB_KVO" is created first. :?: :!:
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
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Detect change between Light and Dark Mode?

Post by wilbert »

mk-soft wrote:But how do you know that the Key Value Observer at Purebasic is called "PB_KV0" to connect it with your own "KVO" :?:
It's just a name. "MKObserver" or any other class name not already used by the operating system or PureBasic will do.
You just need an object of a class that implements "observeValueForKeyPath:ofObject:change:context:" .

One way that is used by other code posted on the forum is to use the app delegate class for this and add the method to that.
But since I don't know if PureBasic itself already uses Key-Value observing (or will do so in the future), it's safer to create a new class instead of using the app delegate class for this.

Code: Select all

objc_allocateClassPair_(objc_getClass_("NSObject"), "PB_KVO", 0)
creates a new class named PB_KVO which extends the NSObject class.
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
mk-soft
Always Here
Always Here
Posts: 5335
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: Detect change between Light and Dark Mode?

Post by mk-soft »

Can the module "osxClassPair" also be used for other classes?

Code: Select all

;-TOP

;- Begin Module

DeclareModule osxClassPair
  Declare CreateClassObject(ClassName.s, RegisterName.s, *Callback, Args.s)
  Declare DisposeClassObject(ClassName.s)
EndDeclareModule

; ---

Module osxClassPair
  
  ImportC ""
    objc_disposeClassPair(*Class)
  EndImport
  
  Structure udtClass
    *Class
    *Object
  EndStructure
  
  Global NewMap Class.udtClass()
  
  Procedure CreateClassObject(ClassName.s, RegisterName.s, *Callback, Args.s)
    Protected *Class, *Method, *Object
    *Class = objc_allocateClassPair_(objc_getClass_("NSObject"), ClassName, 0)
    If Not *Class
      ProcedureReturn 0
    EndIf
    *Method = class_addMethod_(*Class, sel_registerName_(RegisterName), *Callback, Args)
    If Not *Method
      objc_disposeClassPair(*Class)
      ProcedureReturn 0
    EndIf
    If Not objc_registerClassPair_(*Class)
      objc_disposeClassPair(*Class)
      ProcedureReturn 0
    EndIf
    *Object = CocoaMessage(0, 0, ClassName + " new")
    If Not *Object
      objc_disposeClassPair(*Class)
      ProcedureReturn 0
    EndIf
    Class(ClassName)\Class = *Class
    Class()\Object = *Object
    ProcedureReturn *Object
  EndProcedure
  
  Procedure DisposeClassObject(ClassName.s)
    If FindMapElement(Class(), ClassName)
      CocoaMessage(0, Class()\Object, "release")
      objc_disposeClassPair(Class()\Class)
      DeleteMapElement(Class())
    EndIf 
  EndProcedure
  
EndModule

;- End Module

CompilerIf #PB_Compiler_IsMainFile
  
  UseModule osxClassPair
  
  EnumerationBinary
    #NSKeyValueObservingOptionNew
    #NSKeyValueObservingOptionOld
  EndEnumeration
  
  Global *NSKeyValueChangeNewKey.Integer = dlsym_(#RTLD_DEFAULT, "NSKeyValueChangeNewKey")
  Global *NSKeyValueChangeOldKey.Integer = dlsym_(#RTLD_DEFAULT, "NSKeyValueChangeOldKey")
  
  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
  
  ; Create Key-Value Observer class (PB_KVO)
  KVO = CreateClassObject("PB_KVO", "observeValueForKeyPath:ofObject:change:context:", @KVO(), "v@:@@@^v") 
  
  ; add observer
  Global NSApp.i = CocoaMessage(0, 0, "NSApplication sharedApplication")
  CocoaMessage(0, NSApp, "addObserver:", KVO, "forKeyPath:$", @"effectiveAppearance", "options:", #NSKeyValueObservingOptionNew, "context:", #nil)
  
  Procedure.i GetCocoaColor(ColorName.s) ; By wilbert: https://www.purebasic.fr/english/viewtopic.php?p=419571#p419571
    Protected.i Result, Rect.NSRect, Image, NSColor = CocoaMessage(#Null, #Null, "NSColor " + ColorName)
    If NSColor
      Rect\size\width = 1
      Rect\size\height = 1
      Image = CreateImage(#PB_Any, 1, 1)
      StartDrawing(ImageOutput(Image))
      CocoaMessage(#Null, NSColor, "drawSwatchInRect:@", @Rect)
      Result = Point(0, 0)
      StopDrawing()
      FreeImage(Image)
      ProcedureReturn Result
    Else
      ProcedureReturn -1
    EndIf
  EndProcedure
  
  ; ---
  
  Global Event, text_color, control_background_color
  
  Procedure UpdateColors()
    text_color = GetCocoaColor("textColor")
    control_background_color = GetCocoaColor("controlBackgroundColor")
    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, 150, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
  CanvasGadget(0, 10, 10, WindowWidth(0) - 20, WindowHeight(0) - 20)
  
  UpdateColors()
  
  Repeat
    Event = WaitWindowEvent()
    If Event = #EventChangeAppearance
      UpdateColors()
    EndIf
  Until Event = #PB_Event_CloseWindow
  
  ; remove observer
  
  CocoaMessage(0, NSApp, "removeObserver:", KVO, "forKeyPath:$", @"effectiveAppearance")
  DisposeClassObject("PB_KVO")
  
CompilerEndIf
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
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Detect change between Light and Dark Mode?

Post by wilbert »

mk-soft wrote:Can the module "osxClassPair" also be used for other classes?
I'm not sure what you exactly mean with using that module for other classes :?
For the current example, your code is fine.
Windows (x64)
Raspberry Pi OS (Arm64)
Post Reply