Lazarus

Programming => Widgetset => Cocoa => Topic started by: Hansaplast on November 05, 2018, 04:12:56 pm

Title: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 05, 2018, 04:12:56 pm
The latest SVN version (r59451) seems to include some impressive work on support for Dark and Light Theme under Mojave. When switching theme, the IDE changes as well. This even works with my own applications now! Awesome!!  8)


I was wondering if there is an event that I can tie a procedure to so I can have my application respond to the color changes (for custom drawn controls etc)?
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: CCRDude on November 05, 2018, 05:15:36 pm
I would like to second that request :)

Or simply a way to query as a first step.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 05, 2018, 05:29:27 pm
Hi CCRDude!


I found a way to query if Dark Theme is active or not, even if it's not the most elegant way, but it's fast and it works.
The preferences value updates right away when a user switched back and forth between dark and light theme.
You can glue this in one function of course, I just use the GetPrefString function for other purposes as well.



Code: Pascal  [Select][+][-]
  1. // Retrieve key's string value from user preferences. Result is encoded using NSStrToStr's default encoding.
  2. function GetPrefString(const KeyName : string) : string;
  3. begin
  4.   Result := NSStringToString(NSUserDefaults.standardUserDefaults.stringForKey(NSStr(@KeyName[1])));
  5. end;
  6.  
  7.  
  8. // IsDarkTheme: Detects if the Dark Theme (true) has been enabled or not (false)
  9. function IsDarkTheme:boolean;
  10. begin
  11.   Result := pos('DARK',UpperCase(GetPrefString('AppleInterfaceStyle')))>0;
  12. end;
  13.  
   


Note: I got the function to get the preference keyword from one of Phil's units (https://macpgmr.github.io/ObjP/nsunits-src.zip) if I recall correctly - there are some very neat and useful tools in his units.
The biggest problem I run into is that I need to update other controls as well to match the new colors, but have no means to get alerted about the change  :o
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 09, 2018, 03:28:10 pm
Using the function above, I did find a way to detect the change by using the onPaint event.
However I'm not happy, or maybe I should say "concerned", with how often the onPaint event is fired, especially when resizing a form.


Code: Pascal  [Select][+][-]
  1. procedure TForm1.FormPaint(Sender: TObject);
  2. begin
  3.   if IsDarkTheme then
  4.     label1.Caption:='Dark Theme'
  5.   else
  6.     label1.Caption:='Light Theme';
  7. end;


I'd rather see a specific event, based on theme change. I've tried looking at something like this (https://stackoverflow.com/questions/51672124/how-can-it-be-detected-dark-mode-on-macos-10-14) (usign addObserver, with themeChanged() being the notification function);


Code: C  [Select][+][-]
  1. [NSDistributedNotificationCenter.defaultCenter addObserver:self selector:@selector(themeChanged:) name:@"AppleInterfaceThemeChangedNotification" object: nil];
  2.  
  3.  
  4. -(void)themeChanged:(NSNotification *) notification {
  5.     NSLog (@"%@", notification);
  6. }


But I have not been successful in translating this to Pascal, even with Dmitry's help (http://forum.lazarus.freepascal.org/index.php/topic,42577.msg297489.html#msg297489) who suggested looking at (in cocoawsforms)
Code: Pascal  [Select][+][-]
  1. NSNotificationCenter.defaultCenter.addObserver_selector_name_object(cnt, objcselector('didResignKeyNotification:'), NSWindowDidResignKeyNotification, cnt.window);


Any help would be much appreciated.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: jwdietrich on November 25, 2018, 11:19:53 pm
For me, it works great. The onPaint event seems to be automatically fired every time the theme is changed.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 26, 2018, 10:53:41 am
It works fine indeed, just the when resizing a form this event gets fired tons of times - that was my only concern.
You can try with a simple example; add a TLabel to your form, and do the following on the onPaint event:


Code: Pascal  [Select][+][-]
  1.   Label1.Tag := Label1.Tag+1;
  2.   Label1.Caption:=IntToStr(Label1.Tag);


Now run you application and resize the window ...



On top of that; Cocoa has a function to have the OS inform your application that the theme has changed - which would be only one single call when the theme changes.
I have not been able to get that call to work with lazarus though, due to my lack of experience with converting Obj-C calls.
Disk Arbitration has a similar function (called in a different way) to let you application know if a disk was inserted, ejected, mounted or unmounted as well, and that one I did get to work.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: lucamar on November 26, 2018, 02:20:22 pm
Hansaplast, I think at least part of your problem is that if you set the caption in the OnPaint handler it will trigger another repaint and another call to OnPaint.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 26, 2018, 02:36:08 pm
Oh duh! Good call lucamar!  :-[

I'll test it without changing the label and see what happens.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on November 26, 2018, 02:53:37 pm
Hansaplast, I think at least part of your problem is that if you set the caption in the OnPaint handler it will trigger another repaint and another call to OnPaint.



I did a simple test, and even though it's not 100% accurate, it seems that changing the caption does not trigger the onPaint event of the form itself (only a repaint of TLabel?).


Simple resizing of the form to a little bigger, triggered onPaint around 20 times, without updating the TLabel Caption.
Doing the same test, however with updating the TLabel caption, triggered it around 20 times as well.
Since it's kinda hard to measure, it all depends how steady my hand is and if I have the mouse travel the exact same direction and distance at the same speed, I'm guessing onPaint triggers about the same amount of times during a resize, with ot without changing TLabel.caption.


I tried playing with TForm.IsResizing, but that doesn't seem to be doing anything (ie. this value doesn't appear to be "true" at any time while resizing).


Example code I used - simple form, with a TLabel on it:


Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2. {$mode objfpc}{$H+}
  3. interface
  4. uses
  5.   Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;
  6. type
  7.   { TForm1 }
  8.   TForm1 = class(TForm)
  9.     Label1: TLabel;
  10.     procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
  11.     procedure FormCreate(Sender: TObject);
  12.     procedure FormPaint(Sender: TObject);
  13.   private
  14.     counter:integer;
  15.   public
  16.   end;
  17. var
  18.   Form1: TForm1;
  19. implementation
  20. {$R *.lfm}
  21. { TForm1 }
  22. procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
  23. begin
  24.   ShowMessage(IntToStr(counter)); // show count
  25. end;
  26. procedure TForm1.FormCreate(Sender: TObject);
  27. begin
  28.   Counter:=0;
  29. end;
  30. procedure TForm1.FormPaint(Sender: TObject);
  31. begin
  32.   inc(Counter);
  33.   //label1.Caption:=IntToStr(counter);
  34. end;
  35. end.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: VTwin on December 09, 2018, 03:16:59 pm
I notify forms of events, such as changes in file or item selections by sending a message:

Code: Pascal  [Select][+][-]
  1. type
  2.   TVMessage = packed record
  3.     Id    : longint;
  4.     Param : word;
  5.     Flag  : word;
  6.     Text  : shortstring;
  7.   end;
  8.  
  9. { DispatchMessage
  10.   Dispatches a message to all forms. This adds a message to the message queue,
  11.   which will be processsed by the handler after the current process finishes.
  12.   Define a message id such as:
  13.     LM_Msg = LM_USER + 1;
  14.   where LM_USER is defined in LMessages, and a handler such as:
  15.     procedure OnMessage(var msg: TLMessage); message LM_Msg; }
  16. procedure DispatchMessage(Id: dword; Param, Flag: word; Txt: string);
  17. var
  18.   i: integer;
  19.   f: TForm;
  20.   m: TVMessage;
  21. begin
  22.   m.Id := Id;
  23.   m.Param := Param;
  24.   m.Flag := Flag;
  25.   m.Text := Txt;
  26.   for i := 0 to Screen.FormCount - 1 do begin
  27.     f := Screen.Forms[i];
  28.     if (f <> nil) then
  29.       f.Dispatch(m);
  30.   end;
  31. end;
  32.  

In my case, when the user selects a data item an image in another form is updated. Of course this could be modified for objects other than forms.

You could test for a change in dark theme in a timer, then dispatch a message. Any forms with custom controls could react as needed. Just a thought, there is probably a better way.

Cheers,
VTwin
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on December 09, 2018, 05:08:02 pm
Thanks VTwin! I'll give that a try.
Not crazy about using a timer though, but I'll give it a try.
OnPaint does seem to work OK and not bog down the application too much.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: VTwin on December 09, 2018, 05:37:46 pm
Not crazy about using a timer though, but I'll give it a try.

I try not to use them too much, but they can be handy. I had been using timers to set and check global flags for changes requiring updates, but that started getting messy and I mostly switched to messages.

I assume people don't switch themes very often, so maybe a small lag is not a big deal? If the timer checked once every 1000 ms (or more) perhaps that is enough. A simpler option than messages is to just set a global flag, "gIsDarkTheme" in the timer. The controls would only get updated when a repaint is triggered, but maybe that is sufficient, and is similar to what you are doing now.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Clover on August 26, 2020, 08:46:56 pm
The 'AppleInterfaceStyle' method doesn't work in "Auto" mode (introduced in Catalina), which switches between Light and Dark depending on time of day.

The following updated test works correctly on Mojave, Catalina and Big Sur:

Code: Pascal  [Select][+][-]
  1. function isMacDarkMode: Boolean;
  2. var sMode: string;
  3. begin
  4.   //sMode  := CFStringToStr( CFStringRef( NSUserDefaults.StandardUserDefaults.stringForKey( NSSTR('AppleInterfaceStyle') ))); // Doesn't work in auto mode
  5.   sMode  := CFStringToStr( CFStringRef( NSApp.effectiveAppearance.name ));
  6.   Result := Pos('Dark', sMode) > 0;
  7. end;
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: VTwin on August 30, 2020, 05:08:36 pm
The 'AppleInterfaceStyle' method doesn't work in "Auto" mode (introduced in Catalina), which switches between Light and Dark depending on time of day.

The following updated test works correctly on Mojave, Catalina and Big Sur:
...

Thanks!

Can you give the required uses files?
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: trev on August 31, 2020, 02:24:02 am
@VTwin: See the Wiki article (https://wiki.lazarus.freepascal.org/Dark_theme) for a simple demo I wrote.
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Hansaplast on August 31, 2020, 11:27:28 am
Not sure how others do this, but I check for Dark Theme during the Form's onPaint event - so the application can change certain things (for example controls that do not respond well to a theme change, or controls that use non-standard colors). This works for all my apps just fine. Well, except for Windows, since Lazarus doesn't change colors when you change theme in Windows.
If this is the correct way to do this; maybe we can add this as a tip to the Wiki page?
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: trev on September 01, 2020, 04:18:13 am
If this is the correct way to do this; maybe we can add this as a tip to the Wiki page?

I think it's a hack ;)

You mentioned the "correct" method in your earlier 2018 post in this thread... except you didn't manage to get it working :( It's beyond me too...

In which case, a hack may be better than nothing :)
Title: Re: How to detect if macOS changed Theme (Dark vs Light) while running?
Post by: Wallaby on November 24, 2022, 12:32:27 am
I know it's an old thread, but I faced the same problem. I needed to adjust certain custom colours when macOS switches to Dark or Light theme.

Here is the code that will receive the event whenever the theme is changed, manually or automatically:

Code: Pascal  [Select][+][-]
  1. unit Core.MacOS.DarkMode;
  2.  
  3. {$MODESWITCH OBJECTIVEC1}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, CocoaAll, MacOsAll;
  9.  
  10. implementation
  11.  
  12. type
  13.   TThemeChangedNotification = objcclass(NSObject)
  14.     procedure ThemeChangedNotification(notification: NSNotification);
  15.       message 'ThemeChangedNotification:';
  16.   end;
  17.  
  18. procedure TThemeChangedNotification.ThemeChangedNotification(
  19.   notification: NSNotification);
  20. begin
  21.   // This will be triggered upon theme change
  22. end;
  23.  
  24. var
  25.   ThemeChangedNotification: TThemeChangedNotification;
  26.   DistributedNotificationCenter: NSDistributedNotificationCenter;
  27.  
  28. initialization
  29.   ThemeChangedNotification := TThemeChangedNotification.alloc.init;
  30.   ThemeChangedNotification.retain;
  31.   DistributedNotificationCenter := NSDistributedNotificationCenter.defaultCenter;
  32.   if Assigned(DistributedNotificationCenter) then
  33.     DistributedNotificationCenter.addObserver_selector_name_object(
  34.       ThemeChangedNotification, ObjCSelector('ThemeChangedNotification:'),
  35.       NSSTR('AppleInterfaceThemeChangedNotification'), nil);
  36.  
  37. finalization
  38.   ThemeChangedNotification.release;
  39.  
  40. end.
TinyPortal © 2005-2018