kordamp / ikonli

Icon packs for Java applications
http://kordamp.org/ikonli/
Apache License 2.0
502 stars 50 forks source link

Icons can't be styled via user agent stylesheet #121

Closed dlemmermann closed 2 years ago

dlemmermann commented 4 years ago

It seems like it is not possible to style an icon via the "user agent stylesheet" of a custom control (using a skin). When you run the standalone app below you will notice that styling works fine but when you comment out the line getStylesheets().add(IkonliBug.class.getResource("styles.css").toExternalForm()); then only the user agent stylesheet will be used (returned via the getUserAgentStylesheet() method) and styling no longer works.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.stage.Stage;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.materialdesign.MaterialDesign;

public class IkonliBugApp extends Application {

    @Override
    public void start(Stage stage) {
        IkonliBug bug = new IkonliBug();
        Scene scene = new Scene(bug);
        stage.setTitle("Ikonli Bug?");
        stage.setScene(scene);
        stage.setWidth(400);
        stage.setHeight(400);
        stage.centerOnScreen();
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

    public class IkonliBug extends Control {

        private final FontIcon fontIcon = new FontIcon(MaterialDesign.MDI_UPLOAD);

        public IkonliBug() {
            getStyleClass().add("ikonli-bug");

            /*
             * We need to also add the stylesheet directly as otherwise the styling for the
             * ikonli font icon will not work. Bug in Ikonli?
             */
            getStylesheets().add(IkonliBug.class.getResource("styles.css").toExternalForm());
        }

        public FontIcon getFontIcon() {
            return fontIcon;
        }

        @Override
        protected Skin<?> createDefaultSkin() {
            return new IkonliBugSkin(this);
        }

        @Override
        public String getUserAgentStylesheet() {
            return IkonliBug.class.getResource("styles.css").toExternalForm();
        }

    }

    public class IkonliBugSkin extends SkinBase<IkonliBug> {

        public IkonliBugSkin(IkonliBug view) {
            super(view);
            getChildren().setAll(view.getFontIcon());
        }
    }
}
dlemmermann commented 4 years ago

The CSS ins styles.css:

.ikonli-font-icon {
    -fx-icon-size: 48px;
    -fx-icon-color: blue;
    -fx-icon-code: mdi-camera;
}
dlemmermann commented 4 years ago

According to the Javadocs of getUserAgentStylesheet() this should work. We are using this method heavily in ControlsFX, too.

An implementation may specify its own user-agent styles for this Region, and its children, by overriding this method. These styles are used in addition to whatever user-agent stylesheets are in use. This provides a mechanism for third parties to introduce styles for custom controls.

dlemmermann commented 3 years ago

Any idea, yet, how this can be fixed / should be fixed?

aalmiray commented 3 years ago

Unfortunately no, I've got no clue why it's not working.

palexdev commented 3 years ago

I suspect this to be a JavaFX bug Lately I'm having this issue a lot in my custom controls and to change the style of a sub component I have to force it by calling getStyesheets().setAll()

Also in the newer JavaFX versions the documentation has been updated to add:

Subclasses overriding this method should not  
assume any particular implementation approach as  
to the number and frequency with which it is called.  
For this reason, attempting any kind of dynamic  
implementation (i.e. returning different user agent stylesheet values)   based on some state change is  
highly discouraged, as there is no guarantee when,  
or even if, this method will be called. Some JavaFX  
CSS implementations may choose to cache this  
response for an indefinite period of time, and   therefore there should be no expectation around  
when this method is called.  

I wonder if that could be the problem, but I'm just guessing

palexdev commented 3 years ago

@aalmiray @dlemmermann I finally found the issue and an alternative workaround.

JavaFX' StyleableProperties have a way to specify the origin of the value, see here [Docs](https://openjfx.io/javadoc/17/javafx.graphics/javafx/css/StyleableProperty.html#getStyleOrigin()) If you set the value of a StyleableProperty via code (with a setter) it will set the origin to USER and ignore the USER_AGENT stylesheet. So basically when you use the constructor you're setting the icon properties via code which then stops the user agent from working. A workaround is to override the getStyleOrigin() method to always return USER_AGENT.

Here's the same example @dlemmermann provided, but I built a custom version of ikonli that implements such workaround, you can see the difference by switching the dependencies in gradle. Example

Edit: here's what I mean by overriding getStyleOrigin() image

aalmiray commented 3 years ago

Thank you @palexdev! This looks promising. Wonder if it would work as low as JavaFX 11, though with recent version of JavaFX (such as 17) still being binary compatible with Java 11 then I might bump Ikonli all the way to JavaFX 17.

palexdev commented 3 years ago

Thank you @palexdev! This looks promising. Wonder if it would work as low as JavaFX 11, though with recent version of JavaFX (such as 170 still being binary compatible with Java 11 then I might bump Ikonli all the way to JavaFX 17.

Ah yes, I like to always use the latest releases I can test it with JDK11 + JFX11 and report back

dlemmermann commented 2 years ago

Thank you guys!