Morphs are visual objects that stay at the basis of the user interface framework in Pharo. Among other things, they also come with a mechanism that allows us to define look-and-feel themes for these morphs. Technically, this is achieved by having the morphs delegate the relevant rendering code to the UITheme
and its subclasses.
This fine for a user as the only thing you have to do for choosing a new theme is to choose another subclass of UITheme
. However, code-wise it does not work well: over time UITheme
accumulated too many methods and it simply became too difficult to manage.
How can we improve this situation?
First, let’s take a closer look at UITheme
. To give you an idea of how large this class became, executing:
(Object withAllSubclasses sorted: [ :a :b | a selectors size > b selectors size ]) indexOf: UITheme
... says it is the second largest class in Pharo in terms of amount of methods. Indeed, it has 574 methods. That is pretty large.
Are these methods working with one another? The graph below shows all methods as gray nodes and attributes as blue nodes from the UITheme, and the edges show the self message sends and usages of instance variables.
If you are curious about how I produced the image, I simply used a Roassal script and the code model of Pharo. The code is a bit more complicated than it should be just because the the code model does not provide straightforward predicates, and in this case I had to recourse to explicit traversals of the abstract syntax tree:
view := ROMondrianViewBuilder new. class := UITheme. view shape ellipse withoutBorder; fillColor: Color blue. view nodes: class instVarNames. view shape ellipse withoutBorder; fillColor: Color gray. view nodes: class methods. view edges: class methods from: #yourself toAll: [ :each | each parseTree allChildren select: [ :n | n isMessage and: [ n isSelfSend and: [ class methods anySatisfy:[ :m | m selector = n selector ] ] ] ] thenCollect: [ :n | class methods detect: [:m | m selector = n selector ] ]]. view edges: class methods from: #yourself toAll: [ :each | each parseTree allChildren select: [ :n | n isVariable and: [ class instVarNames includes: n name ] ] thenCollect: #name ]. view forceBasedLayout. view open
There are several things we can learn from this graph. The instance variables are only used in a couple of disparate methods. But, there appears to exist some communication between methods. On a closer inspection, we come to see that much of the graph connectivity happens due to only a few methods. As an exercise, we displayed the same class again, only by removing three methods: icons
settings
textFont
.
As you can see, the connectivity, and thus the class cohesion, goes down dramatically.
If these methods do not really work together, why did they get in this class? Essentially, they are actually related to the various morphs class, and the only reason to have them in the UITheme
class is so that subclasses can provide different look and feels by overriding the methods.
For example, DialogWindow
defines the fill style by delegating to the UITheme
:
DialogWindow>>activeFillStyle ^self theme dialogWindowActiveFillStyleFor: self
UITheme>>dialogWindowActiveFillStyleFor: aWindow ^aWindow paneColorToUse lighter
Thus, subclasses of UITheme
can override dialogWindowActiveFillStyleFor:
and provide a different style. This design obviously does not scale well.
Another design would be to add another level of indirection by introducing the notion of a Themer
. Essentially, rather then having the UITheme
be responsible for holding the actual rendering code, we make it be a collection of strategy objects, each dedicated to a morph. Let me exemplify what I mean on a case study.
Sean DeNigris recently created a rather beautiful incarnation of the Mac Spotlight in Morphic. It looks like this.
Now, let’s make it theme-able. Coloring is the primary target for theming. For example, the SpotlightItemMorph defines the color for the selected item like this:
SpotlightItemMorph>>onSelected self color: (Color r: 0.2627 g: 0.4588 b: 0.9333).
Let’s transform it. First, we add the themer object to UITheme
:
UITheme>>spotlightThemer ^ SpotlightThemer new
Afterwards, the onSelected
simply delegates to the themer that is specific to the current theme:
SpotlightItemMorph>>onSelected self theme spotlightThemer configureSelectedItemMorph: self
And the default implementation remains the same:
SpotlightThemer>>configureSelectedItemMorph: aMorph aMorph color: (Color r: 0.2627 g: 0.4588 b: 0.9333)
At this point, if we want to provide another rendering, we can do it by subclassing SpotlightThemer
and by overriding the spotlightThemer
method in a UITheme
subclass.
With this design, we still have the UITheme
be in charge of producing a cohesive theme, but it does so by delegating the actual code to specialized themer objects. It should scale better.
To finish the job, we need to do the same for all the Spotlight methods that refer to Color. To do this, we write a query and use the GTInspector to guide us through the code:
(RPackageOrganizer default packageNamed: 'Spotlight') methodReferences asSet select: [ :each | (each literals collect: #value) anySatisfy: [:literal | literal = Color ] ]
There are four methods in Spotlight that refer to Color
, and theme-ing them is almost straightforward now that we have the tool to browse them.
Just because Pharo is a dynamic language, it does not mean that static analysis is useless. There is a wealth of information still left in the structure. The key thing is to ask the right question and know that there are powerful tools around to help. In my case, I used a query and a custom visualization to reason about a code problem, and I guided a specific refactoring by means of a query and a browser. The whole exercise took less than half an hour (and significantly longer to describe it here).
By the way, there are plenty of Morphs around that hardcode the reference to Color. How many? Inspect the result of this script:
(Morph withAllSubclasses flatCollect: [ :c | c methods collect: #asRingDefinition ]) select: [ :each | (each literals collect: #value) anySatisfy: [:literal | literal = Color ] ]
In my image, it reveals 363 such methods.