Qt Qt Quarterly
Qt Development Frameworks | Documentation | Qt Quarterly
« Designing Custom Controls with PyQt

Adaptive Coloring for Syntax Highlighting

by Helder Correia

A feature present in many textual applications is the use of syntax coloring and highlighting. From source code editors to expression parsers and mathematics software, all kinds of software offer the possibility to assign different colors to different classes of terms. In this article, we'll take the coloring convenience one step further: let's make the machine choose the colors for us.

The problem with traditional manual color choosing is that once the user decides to change the system color scheme (including the base color for text input widgets), it will probably ruin the visibility of the syntax highlighting. The traditional solution is to change the previously chosen syntax highlighting color scheme in a tedious manual process. This then needs to be repeated each time the user gets bored with the system colors.

A Modern Approach

Computers were invented to help us, solve our problems, make our lives easier and do the dirty jobs for us. And all of this in an automatic way. So why should the user still need to bother to make sure that all applications featuring syntax highlighting really adapt well to a new system color scheme? And why should developers need to bother about all the different default color schemes across the multiple desktop and mobile platforms where their products need to be deployed?

Life can be so much easier and more productive if you can allow yourself the luxury (according to today's standards) of ignoring all this and let software adapt itself. In an ideal world, software should be able to morph completely color-wise. For instance, icons with massive usage of blue should turn red, yellow, white or something else when faced with an unexpected blue background. But that's a topic for a future article—let's focus on this one.

The Proof of Concept Solution

The algorithm proposed in this article is far from being perfect and assumes some user decisions that may not happen all the time, such as choosing highly-contrasting foreground and background colors. Although our main goal is to raise awareness of this topic, the technical results are themselves very satisfactory. So much so, that the code used here is in already in production, serving an existing open source project with no user dissatisfaction so far.

The simplicity and size of the algorithm may sound ridiculous to someone who is unfamiliar with color theory, but Qt makes things easier for us with its great color facilities. There are essentially three input factors:

The output will simply be a list of generated colors. Not only will this list omit both the background and foreground colors, but it will also offer a reasonable contrast against them.

70% of the solution was achieved with a simple color rotation. The HSV color model was the natural choice to attack the problem with, since it provides a very easy method to play with color contrast and, fortunately, it is readily available in Qt.

The HSV Color Space

The Hue Saturation Value color space is just a convenient representation of points in the familiar RGB color space that allows fast perceptual color relationships. It is usually interpreted as a cylinder, using the standard cylindrical coordinates to represent each component.

An illustration of the HSV color space.

The hue component defines the base color and its value range is 0 to 359 (degrees of arc, in this representation). In Qt, a hue of 360 has exactly the same meaning as 0, while 361 is equivalent to 1, and so on. Monochromatic (grayscale) colors have a hue of -1.

Next comes saturation, which defines the intensity of the color chosen, represented as a radial component in the above diagram. Its value varies from 0 to 255.

Finally, the value parameter sets the brightness of the color, and is represented as a vertical component along the axis of the cylinder in the above diagram. The value must again be in the range of 0 to 255.

The color selection part of QColorDialog.

Another representation of the HSV color space that may be familiar to users is the one used provided by QColorDialog. The hue parameter is measured along the x-axis in the color gradient box and the saturation parameter is measured along the y-axis.

The value parameter is set separately in the vertical gradient to the right of the box. The user typically chooses the hue and saturation of a color before fine-tuning the value.

A detailed description of the model is available in the QColor documentation.

From Theory to Algorithm

The highlight() function we use to implement our color selection algorithm is as simple as possible—it accepts the background color, the foreground color, and the number of output colors required. From these values, it derives a vector of colors to return. The function looks like this:

    QVector<QColor> highlight(const QColor &bg, const
                              QColor &fg, int noColors)
    {
        QVector<QColor> colors;
        const int HUE_BASE = (bg.hue() == -1) ? 90 : bg.hue();

The colors vector will be used as the result container. HUE_BASE is the starting angle for the hue rotation. If the background is achromatic (it has a hue of -1) then we start at 90 degrees (somewhere between yellow and green), otherwise we start at the same value as the background color.

        int h, s, v;
        for (int i = 0; i < noColors; i++) {
            h = int(HUE_BASE + (360.0 / noColors * i)) % 360;
            s = 240;
            v = int(qMax(bg.value(), fg.value()) * 0.85);

Now, we iterate through the number of colors to generate, and perform a hue rotation of as many degrees as possible between the generated colors within a full rotation (360 divided by number of colors to generate). This is the most important step of the process and, once understood, HSV can be easily applied to solve many problems.

The saturation value is fixed, so colors always have good intensity, which generally helps to establish a good level of contrast.

The value (or brightness) of the color is set to be slightly lower than the highest brightness of the foreground and background colors.

Actually, we could stop here, but then the results would not be satisfactory in many situations, so some corner cases need to be handled. The remaining part of the loop is not particularly scientific or interesting, but the result of many hours of trial and error combined with some real logic, and leads to satisfactory results.

It also tries to ensure that the foreground color is not selected for highlighting (that's the whole point). So if a very similar color has been generated, it modifies its parameters so that there will be a contrast between them.

            const int M = 35;
            if ((h < bg.hue() + M && h > bg.hue() - M)
                || (h < fg.hue() + M && h > fg.hue() - M))
            {
              h = ((bg.hue() + fg.hue()) / (i+1)) % 360;
              s = ((bg.saturation() + fg.saturation() + 2*i)
                  / 2) % 256;
              v = ((bg.value() + fg.value() + 2*i) / 2)
                  % 256;
            }

Since this is not a scientific paper and it only covers boring corner cases, let's skip the logic behind the code above and just assume it does a fairly nice job.

      colors.append(QColor::fromHsv(h, s, v));
      }
    
      return colors;
    }

For each set of components, we create a new color and append it to the vector, which we eventually return.

Note that, throughout the function, we refer to the hue, saturation and value of the two input colors to perform calculations entirely in HSV space. Even when we create and append a new color to the vector, we do not change its representation.

The Mandatory Demo Application

To demonstrate the use of our algorithm, we have created a demo application to generate some suitable highlighting colors for a given pair of foreground and background colors.

The screenshots on the following page show the algorithm in action in our demo application. You can manually choose both the foreground and background colors used, or simply choose one of the predefined color schemes. Among them are the traditional white on black, found by default in the vast majority of systems, and color schemes for Solaris and CDE.

The number of generated colors is fixed to three, presented in the form of colored squares in the box to the right, which is itself filled with the chosen background color.

Autohilite-Whiteonblack Autohilite-Whiteondarkblue

Autohilite-Greenonblack Autohilite-Yellowonblue

Clockwise from top-left: White on black, white on dark blue, yellow on blue and green on black.

By playing with the red, green and blue values of each color, you can see how the algorithm generates new combinations of colors. Try to avoid choosing foreground and background colors that are too close to each other; our technique works best if these colors are visually distinct.

Since we provide a simple, self-contained function to perform the work of generating these colors, we can easily adapt it for use in any application that uses syntax highlighting.

Use Cases

This automatic coloring for syntax highlighting idea came up during the development of the cross-platform Qt 4-based calculator SpeedCrunch (http://speedcrunch.org). It offers highlighting for variables, functions and numbers within its supported mathematical expressions.

Speedcrunch

We can also apply the technique to Qt's Syntax Highlighter example. However, because it was not designed to be extensible, some changes are required. The version accompanying this article includes a dialog, available via the Settings menu, that lets the user generate a new color scheme for a given pair of foreground and background colors.

Syntaxhighlighter1 Syntaxhighlighter3

Syntaxhighlighter4 Syntaxhighlighter2

Although the algorithm generates reasonably distinct colors for a good pair of starting colors, we also let the user modify each of the generated colors using standard color selection dialogs.

A more sophisticated interface could be developed that lets users choose from a suggested palette of colors, adapting the choice as the user picks out each color to use for a highlighting style.

Alternatively, we could define a set of color schemes or themes to satisfy the majority of users, and provide a separate user interface to enable users to configure them, perhaps with the aim of making themes usable across a range of applications.

Plan B and Other Improvements

Although our approach is intended to help the user obtain a nice set of contrasting colors, applications should always provide a "Plan B" mechanism to bypass or override algorithms like this that aim to be perfect. Partly, this is down to user choice—all human brains are different and there will always be users who want to do it their own way—but also because some users have special needs that our algorithm does not take into account.

However, because we want computers to work for us, not the other way around, we can still use this approach to generate a decent starting set of colors that users can fine-tune as they wish, as we have done in the modified Syntax Highlighter example. In some situations, where only a limited number of colors are involved, or where they are used for purely decorative purposes, fine-tuning may not be necessary.

As we noted earlier, similar techniques can also be applied to other issues in user interface design, such as the problem of how to colorize generic icons to suit the user's preferred desktop theme, and potentially even accessibility issues that affect color blind and partially-sighted users.

The code for our examples is available from the Qt Quarterly Web site.

Site Map Accessibility Contact