Change your application theme dynamically.

I recently spent quite some time on a trivial (apparently not) issue. I wanted to deliver custom themes for my application, and allow users to radically change the look with a click in a Menu.

The difficulty went with the fact that I wanted to allow people to modify my application JAR, in order for them to ship their own theme.

Here is how I’ve done it.

First of all, we need to agree on a theme. In my application, it’s a sub-package in the resources providing a stylesheet, and some icons. I provide a mandatory theme called defaultIcons, and each client can add its own. Also note that theme can be incomplete. If a user wants to change the copy and paste icon, but let the rest untouched, it’s possible. I wanted it flexible.

Once we understand that, I needed to dynamically find and load my themes. Well that wasn’t easy because you have a FileSystem with your IDE when developing, and you have a JAR when you install your application. So two different things and I must admit I don’t think my solution is the best.. But at least it’s working!

/**
     * Creates the ChangeTheme Menu by looking inside the package "theme". Every
     * package found is supposed to be an icon package. Inside we may have an
     * "i18n.properties" file which contain the Labels for the files.
     *
     * @return
     */
    private Menu initChangeTheme() {
        Menu item = new Menu("Change Theme");
        //I get the "theme package".
        URL url = MyClass.class.getResource("theme");

        try {
            FileSystem fileSystem = null;
            Path myPath;
            //If I'm installed, I have a JAR.
            if (url.toURI().getScheme().equals("jar")) {
                fileSystem = FileSystems.newFileSystem(url.toURI(), Collections.<String, Object>emptyMap());
                //I need to manually specify my path to the theme package.
                myPath = fileSystem.getPath("com", "myCompagny", "myApp", "theme");
            } else {
                //If I'm working on my IDE, it's smooth.
                myPath = Paths.get(url.toURI());
            }
            Stream<Path> walk = Files.walk(myPath, 1);
            //I create a toggleGroup to group my themes. 
            ToggleGroup toggleGroup = new ToggleGroup();
            for (Iterator<Path> it = walk.iterator(); it.hasNext();) {
                Path path = it.next();
                //I used FileNameUtils of org.apache.commons.io because I was too lazy to implement myself..
                String packageName = FilenameUtils.getName(FilenameUtils.normalizeNoEndSeparator(path.toString()));
                //The "theme" package is returned itself, but we are interested only in the sub-packages.
                if (!"theme".equals(packageName)) {
                    RadioMenuItem menuItem = new RadioMenuItem(findIconPackageName(packageName));
                    menuItem.setToggleGroup(toggleGroup);
                    //I store the current used theme here, so I just check the Menu if it's the one used.
                    if (ImageUtils.ICONS_DIRECTORY.equalsIgnoreCase(ImageUtils.THEME_DIRECTORY + packageName)) {
                        menuItem.setSelected(true);
                    }
                    menuItem.setOnAction((ActionEvent event) -> {
                        //Change theme here.
                    });
                    item.getItems().add(menuItem);
                }
            }
            if (fileSystem != null) {
                fileSystem.close();
            }
        } catch (URISyntaxException | IOException ex) {
            //Throw error
        } 
        return item;
    }

So basically, I find the sub-packages by inspecting my JAR (or fileSystem). I used FilenameUtils or org.apache.commons.io in order to extract the package name from the path (com/myCompagny/Myapplication/theme/NewTheme for example). But I think you can easily do that yourself and get rid of the dependency.

Also, here is the method used to get the package name. I try to find an i18n.properties file inside my sub-package, and extract a custom name. Otherwise, the sub-package name is used :

/**
     * Given a File denoting a Package, we try to isolate the "i18n.properties"
     * in order to get the Label to display in the menu for different languages.
     *
     * @param myPackage
     * @return
     */
    private String findIconPackageName(String myPackage) {
        URL i18n = MyClass.class.getResource("theme/" + myPackage + "/i18n.properties");
        String name = null;
        if (i18n != null) {
            Properties properties = new Properties();
            try {
                properties.load(i18n.openStream());
//Here I just return the current Locale, and used the method "toLanguageTag" on it.
                name = (String) properties.get(getI18nManager().getCountryTag());
            } catch (IOException ex) {
//Handle exception
            }
        }
        if (name == null || name.isEmpty()) {
            name = myPackage;
        }
        return name;
    }

And my property file looks like that :

fr=Mon super thème
en=My awesome theme

Now all you need to do is to load your icons with the same method, that will load images from the current theme, and fallback to the default theme if images are not found :

/**
     * ImageFactory that returns an Image with the given path. The path given
     * has a "/" as prefix, and the name of the icon. This method will add the
     * prefix regarding the current package of icons used. If the icon is not
     * found, we fallback to the defaultIcons package.
     *
     * @param iconName
     * @return
     */
    public static Image getImage(String iconName) {
        if (iconName == null || iconName.isEmpty()) {
            return null;
        }
//It will be for example, "theme/myNewTheme/copy.png"
        String iconNameFull = ICONS_DIRECTORY + iconName;
        //First look in cache
        if (getImageMap().containsKey(iconNameFull)) {
            return getImageMap().get(iconNameFull);
        }
        InputStream input = MyClass.class.getResourceAsStream(iconNameFull);
        if (input == null) {
            //We fallback on the default icon.
            iconNameFull = DEFAULT_ICON_DIRECTORY + iconName;
            if (getImageMap().containsKey(iconNameFull)) {
                return getImageMap().get(iconNameFull);
            }
            input = MyClass.class.getResourceAsStream(iconNameFull);
            if (input == null) {
                return null;
            }
        }
        Image image = new Image(input);
        getImageMap().put(iconNameFull, image);
        return image;
    }

For the Image cache, explanation in a previous article here.
Here is the final result in my application :
theme

If you have any improvements, feel free to comment.

Leave a Reply

Your email address will not be published. Required fields are marked *