The last few years my Xojo work has primarily been maintaining and adding features to existing programs. However, I recently started a brand new project and it appears there are a few others that might be in the pipeline for me, so I thought that this is a great time to Deep-dive into the API2.0 changes and the stuff needed to truly support Dark Mode comprehensively and consistently.
I want to make a good fresh start on API 2.0 and make sure I don’t miss any items on Dark Mode Support.
I’ve found a few issues, but implemented solutions, but wanted to make sure that I’m not missing something fundamental.
Are there any tips and tricks you can share? I am about 60% Mac and 40% Windows in Xojo projects, with about 1/2 of my projects being cross-platform.
You will need to create an Apps > AppearanceChanged Event. In here I call a generic Method I call doSetDarkMode. This cycles through the open windows and sets:
app.Window(tempInt).HasBackgroundColor = Not Color.IsDarkMode 'turns off my white background in dark mode
'set any black Toolbar icon to grey (or back to white)
'set the FillColor of any Rectangle to colour 'Tungsten' (or back to white)
It might be worth creating an interface for this with a designated method. That way all of your “dark mode” code will be in a common place if you need it and you’ll be sure that it gets called.
David, and Greg, Thanks.
I have have added an interface (which adds AppearanceChanged method). I then created a singleton class with a single method which is called from the Application.AppearanceChanged event handler. this method walks through all the windows and controls looking for items implementing my new interface. For each of them, the AppearanceChanged method of the object is called.
This works, but I immediately found a weirdness in Xojo (2022 Release 3). I put a DesktopTextArea in my testing window to display debug messages.
Lo and behold, my stuff worked great in dark → light and back again, but the DesktopTextArea only changed the background, not the text color. It remained black.
So, is this a bug or will I have to sub-class DesktopTextArea just to get Dark Mode to work?
It’s a bug. It should automatically adapt to Dark Mode. So long as you don’t change the default colours.
I’m going to suggest that you only apply the interface to Windows and that you let the windows decide which individual controls need to be updated.
As Ian notes, the built-in controls should change automatically unless you change the colors manually. Apple uses a color called TextColor which is usually Black in light mode and White in dark mode, but can change if Apple decides to. For the purpose of the IDE and the framework, if you leave the text as black, it’ll automatically change. If you make it any other color, it will not.
Well, in the ‘bug’ example above, I didn’t do anything to colors in the DesktopTextArea, absolutely nothing, no event handlers at all, only .AddText(xxx) calls.
Greg, and Ian, I’m wondering why you suggest only informing windows and not controls of the Light/Dark mode changes. Since I’d only add my interface to subclassed controls where I need to do something special for dark mode (i.e. something other than just using the ‘isDarkMode’ value and using colorGroups). An example that has immediately popped up is to modify images used to either the Light or Dark version.
Clearly DarkMode/LightMode changes should not be frequent (i.e. multiple times per second), so there should not be any concern over the processing overhead of walking through all the visual objects (windows and controls).
Finally, do I need to file a report on this DesktopTextArea Dark Mode issue shown above? Thoughts?
Instead of having to cycle through a vast number of items (controls) it is easier to cycle through the windows and let the Window deal with the controls it contains. Cycling the windows is easy in the App class, check if the window supports the interface and if it does call it. For example, put this in the App.AppearanceChanged event:
For nWindow As Integer = 0 To WindowCount - 1
If WindowAt( nWindow ) IsA AppearanceChanged Then
AppearanceChanged( WindowAt( nWindow ) ).AppearanceChanged
Assuming ‘AppearanceChanged’ is an interface you create, defining a Widnow.AppearanceChanged event. Assign it to any window class that requires special action on change in Appearance.
Isn’t this what Color groups are supposed to solve?
If dark mode is done correctly (and excluding edge cases or specialties), you shouldn’t need to do anything.
I would alike to add, that if you want to use appearanceChanged, it might be a good idea to create a feedback case with Xojo asking them to expose “viewDidChangeEffectiveAppearance” so that each control can react when the appearance changes, this IMHO is a far more OOP way of handling it, it also makes it easier for different windows and views to use different themes.
David, Greg, Ian, Sam, and anyone else reading later,
Thanks for the ideas and feedback. I’ve now implemented and tested a number of solutions to the problem of having customized controls which must respond to appearanceChanges. I’ve written a summary of the general approaches, the strengths and weaknesses of each and provided code for two of these different approaches, including the approach that I’ve settled on.
The article is on my company site here: Who’s Afraid of the Dark? - Making Dark Mode Work.
It is unfortunate that AppearanceChanged is not an exposed event for all visual classes. If it existed in DesktopUIControl then all of this would have been moot. But you can’t have everything you might want.
I agree that ColorGroups do solve most of the issues. Add in ColorGroups and color.isDarkMode in your own paint/draw operations and the vast majority of appearance change is covered. But my very first issue was indeed what you might call an edge-case where the icon/graphics used in a canvas need to be switched when the appearance changes.
Again, Thanks everyone.
Would a ColorGroup solve my issue with individual black toolbar icons (needing to be greyed) or white Rectangles needing to be darkened?
For interfaces, always prefer some kind of noun or adjective for its name, like Writer or Writable for something that writes or are able to write. So instead of “AppearanceChanged” interface, it would be better something like an “AppearanceChanger” interface.
Late to the party, but in GraffitiColors I’ve built a cross-platform notification system using delegates where individual objects can register to be notified when the theme switches. In that method I redraw any icons/pictures that are dark mode-aware.
If it is something that is drawn by the system, like say a Rectangle. and you set the BorderColor and/or FillColor using a colorGroup (assuming you use a Dual color or Named color, not a single color of course)
then it will automatically respond to Dark/Light mode changes correctly.
Same for text and labels and other system provided items you set the color of.
I can see the advantages.