Page 1 of 1

Detect change between Light and Dark Mode?

Posted: Thu Apr 11, 2019 4:23 am
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

Re: Detect change between Light and Dark Mode?

Posted: Fri Apr 12, 2019 5:52 pm
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")

Re: Detect change between Light and Dark Mode?

Posted: Fri Apr 12, 2019 6:36 pm
by mk-soft
Thanks :D :wink:

Re: Detect change between Light and Dark Mode?

Posted: Fri Apr 12, 2019 8:55 pm
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.

Re: Detect change between Light and Dark Mode?

Posted: Sat Apr 13, 2019 7:50 am
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.

Re: Detect change between Light and Dark Mode?

Posted: Sat Apr 13, 2019 10:37 am
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. :?: :!:

Re: Detect change between Light and Dark Mode?

Posted: Sat Apr 13, 2019 11:45 am
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.

Re: Detect change between Light and Dark Mode?

Posted: Sat Apr 13, 2019 2:06 pm
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

Re: Detect change between Light and Dark Mode?

Posted: Sat Apr 13, 2019 3:06 pm
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.