mgarin / weblaf

WebLaF is a fully open-source Look & Feel and component library written in pure Java for cross-platform desktop Swing applications.
http://weblookandfeel.com
GNU General Public License v3.0
1.14k stars 235 forks source link

Skin variables #460

Open mgarin opened 7 years ago

mgarin commented 7 years ago

In case your application supports multiple skins it is quite often necessary to tie some specific settings to the specific skin. For example it could be some Color or simple String text (or language key) that cannot really be set into its destination from the style itself, but needs to be bound to each specific skin.

Simple example of this case with Color is ExamplesFrame in WebLaF DemoApplication - it needs to use different element highlights for nodes and, most importantly, for legend text based on WebStyledLabel. While nodes renderer for the tree can actually be configured to provide different states for node highlights you can still only change the whole label foreground, but it would be really hard and inconvenient to change color for different text parts inside single label.

There are also many other small cases like that one where styling system seems limiting or simply missing some functionality that is required - skin variables. To put it simple I want to be able to store various objects (or settings/properties/data - doesn't really matter how you call it) and be able to retrieve them in runtime from the currently used skin.

Those objects might be or might not be used within skin styles, but you should be able to retrieve them from the skin at any given time.

Definition

This is how I currently imagine simple variables would look like in skin's XML:

<skin xmlns="http://weblookandfeel.com">

    <!-- Skin variables -->
    <variables>

        <!-- Legend colors -->
        <beta>180,70,70</beta>
        <release>70,140,70</release>
        <common>black</common>
        <updated>70,70,180</updated>
        <deprecated>125,125,125</deprecated>

    </variables>

    ...

</skin>

Probably it is also worth adding multi-level variables support right away:

<skin xmlns="http://weblookandfeel.com">

    <!-- Skin variables -->
    <variables>

        <!-- Text color -->
        <text>
            <color>
                <normal>black</normal>
                <disabled>120,120,120</disabled>
            </color>
            <size>
                <common>12</common>
                <title>13</title>
                <accelerator>10</accelerator>
                <tooltip>12</tooltip>
            </size>
        </text>

    </variables>

    ...

</skin>

To avoid clunky names like "disabledTextColor" or "tooltipTextSize" and general mess which would certainly be there without any ability to structure custom variables.

Usage in style

I'd love to see quite straightforward variables usage within styles, here is a small example based on the current JLabel component style:

<skin xmlns="http://weblookandfeel.com">

    ...

    <!-- Label -->
    <style type="label">
        <painter>
            <decorations>
                <decoration>
                    <LabelLayout>
                        <LabelIcon constraints="icon" />
                        <LabelText constraints="text" color="{text.color.normal}" />
                    </LabelLayout>
                </decoration>
                <decoration states="disabled">
                    <LabelLayout>
                        <LabelText color="{text.color.disabled}" />
                    </LabelLayout>
                </decoration>
            </decorations>
        </painter>
    </style>

</skin>

Benefits of such variables usage is quite obvious - you can avoid copy-pasting same settings between different components over and over again. Settings like default text colors, font sizes, rounding values, shadow widths etc.

Usage in code

Variables should be easily accessible from any part of the code:

final Color beta = StyleManager.getColor ( "beta" );

Or for structured variables:

final Color color = StyleManager.getColor ( "text.color.normal" );

There should be various convenient methods for most common settings and basic one for Object:

final Color color = StyleManager.getColor ( "text.color.normal" );
final int size = StyleManager.getInteger ( "text.size.normal" );
final Object size = StyleManager.getInteger ( "some.other.object" );

You should also be able to retrieve these variables directly from loaded skin instance:

final Skin skin = new DarkSkin ();
final Color beta = skin.getColor ( "beta" );

Probably access to variables can also be bridged into Web-components to simplify it even further and to avoid static method calls:

final WebLabel label = new WebLabel ( "Test" );
final Color beta = label.getColor ( "beta" );

It would certainly be helpful if you are mostly using Web-components as you won't really ever need to call StyleManager directly. But those extra methods might intersect with existing ones so this part might require further investigation.

Scope

Variables can be defined in any parts of a skin (probably just 1 block per XML file?) and should be accessible right after they have been defined, even if they were defined in a different skin file which was included.

All variables should be gathered in a single tree-like data structure. The ones using same identifiers and having the same type should replace each other, ones having different types should probably throw an exception to notify about obvious issue in the style.

Fonts

There is already a topic with a discussion of a somewhat similar enhancements, just less broad ones - related only to application skins: #331

I'm not yet sure if fonts should be a part of variables system or be a separate thing, so this is certainly a topic open for the discussion.

mgarin commented 6 years ago

We had a discussion with my colleague on the future addition of variables and how they should probably end up looking in the XML to make their usage convenient and we came up with a slightly different approach.

Here is how I initially though they will end up being:

<skin xmlns="http://weblookandfeel.com/XmlSkin">

    <!-- Skin variables -->
    <variables>

        <!-- Light skin -->
        <light>
            <foreground>
                <normal>black</normal>
                <disabled>120,120,120</disabled>
            </foreground>
            <size>
                <common>12</common>
                <title>13</title>
                <accelerator>10</accelerator>
            </size>
        </light>

    </variables>

</skin>

Basically I wanted to provide easy way to structure them allowing complex references like {light.foreground.disabled} to be used in XML styles and code.

Although that concept lacks definition of variable types which will force me to more or less "guess" them every time or to provide some additional way of defining type. Also it is highly questionable whether you would actually need a complex variables structure for the styles as you will still be referencing specific variables by flat identifier and most commonly there will only be 20 to 30 variables in a full-fledged skin.

Also this way variables will lack some agility they might have with a different implementation. To be more specific - various currently existing styles define multiple states only for the sake of changing one or two settings.

So we came up with something different to solve these problems - stateful variables:

<skin xmlns="http://weblookandfeel.com/XmlSkin">

    <!-- Skin variables -->
    <variables>

        <!-- Text foreground -->
        <color id="control">
            <state id="default">black</state>
            <state id="disabled">120,120,120</state>
        </color>

        <!-- Border color -->
        <color id="border.focusable">
            <state id="default">170,170,170</state>
            <state id="focused">85,130,190</state>
            <state id="disabled">lightGray</state>
        </color>

        <!-- Shadow size -->
        <integer id="shadow.outer.small">2</integer>
        <integer id="button.shadow.inner">
            <state id="default">0</state>
            <state id="pressed">6</state>
        </integer>

        <!-- Shadow opacity -->
        <float id="shadow.outer.opacity">
            <state id="default">2</state>
            <state id="pressed">0</state>
            <state id="disabled">0</state>
        </float>

        <!-- Round -->
        <integer id="round.small">2</integer>
        <integer id="round.medium">3</integer>

        <!-- Background -->
        <Background id="button">
            <state id="default">
                <GradientBackground>
                    <color>white</color>
                    <color>223,223,223</color>
                </GradientBackground>
            </state>
            <state id="pressed">
                <ColorBackground color="210,210,210" />
            </state>
        </Background>

    </variables>

</skin>

This approach gives a decent way to define and validate variables in XML as well as allowing us to simplify styles XML by a good chunk. Let's look at current button.xml to see how exactly it can help us:

<skin xmlns="http://weblookandfeel.com/XmlSkin">

    <!-- Button -->
    <style type="button" padding="2,4,2,4">
        <painter>
            <decorations>
                <decoration>
                    <WebShape round="3" />
                    <WebShadow type="outer" width="2" />
                    <LineBorder color="170,170,170" />
                    <GradientBackground>
                        <color>white</color>
                        <color>223,223,223</color>
                    </GradientBackground>
                    <ButtonLayout>
                        <ButtonIcon constraints="icon" />
                        <ButtonText constraints="text" color="black" />
                    </ButtonLayout>
                </decoration>
                <decoration states="focused">
                    <LineBorder color="85,130,190" />
                </decoration>
                <decoration states="pressed">
                    <WebShadow type="outer" opacity="0" />
                    <WebShadow type="inner" width="6" />
                    <ColorBackground color="210,210,210" />
                </decoration>
                <decoration states="disabled">
                    <WebShadow type="outer" opacity="0" />
                    <LineBorder color="lightGray" />
                    <ButtonLayout>
                        <ButtonText color="120,120,120" />
                    </ButtonLayout>
                </decoration>
            </decorations>
        </painter>
    </style>

</skin>

This is the default current button style.

And this is how we can pack this style using variables from above:

<skin xmlns="http://weblookandfeel.com/XmlSkin">

    <!-- Button -->
    <style type="button" padding="2,4,2,4">
        <painter>
            <decorations>
                <decoration>
                    <WebShape round="{round.medium}" />
                    <WebShadow type="outer" width="{shadow.outer.small}" opacity="{shadow.outer.opacity}" />
                    <WebShadow type="inner" width="{button.shadow.inner}" />
                    <LineBorder color="{border.focusable}" />
                    <Background reference="{button}" />
                    <ButtonLayout>
                        <ButtonIcon constraints="icon" />
                        <ButtonText constraints="text" color="{control}" />
                    </ButtonLayout>
                </decoration>
            </decorations>
        </painter>
    </style>

</skin>

Basically we can delegate most (or in some cases all) of the state-related changes to the variables and have a clean style.

You might have also noticed that two different backgrounds are represented by single variable reference - <Background reference="{button}" /> - this is one of the more complex options for variables. The idea is to provide reference-like implementations for each of the base features like IBorder, IBackground or even IContent to allow using them directly from variables as they are also shared between multiple styles.

Potentially that might allow simplifying styles down to something like this:

<skin xmlns="http://weblookandfeel.com/XmlSkin">

    <!-- com.alee.laf.button.WebButton -->

    <!-- Button -->
    <style type="button" padding="2,4,2,4">
        <painter>
            <decorations>
                <decoration>
                    <Shape reference="{button}" />
                    <Shadow reference="{button.outer}" />
                    <Shadow reference="{button.inner}" />
                    <Border reference="{button}" />
                    <Background reference="{button}" />
                    <ButtonLayout>
                        <ButtonIcon constraints="icon" />
                        <ButtonText constraints="text" color="{control}" />
                    </ButtonLayout>
                </decoration>
            </decorations>
        </painter>
    </style>

</skin>

Advantages of this approach are not obvious if you only look at this example as the biggest advantage is that those elements are reusable between different styles without necessity to override or extend anything - you can simply reference desired element where you need it without defining it all over again from scratch.

On the other hand this might interfere with currently existing features like extending and overriding styles as it will be quite hard to tell what exactly going to happen whenever some settings are modified in the extending or overriding style. So eventually I might end up only doing simplified variables.

mgarin commented 4 years ago

Another thing that have been an ongoing discussion is how variables should be modifiable from the code (on top of simply being accessible) and that changing any of them should update all components which styles are using them.

This is something that will require extra work, but not an unreasonable amount because the main rework for variables will provide all means necessary for this feature - mainly non-primitive properties within all style elements, let me elaborate on this one.

Skin variables

I have already figured out the best approach to implement variables and it will require an extensive rework for all properties used in all parts of existing style elements like shapes, shadows, borders, contents etc. The rework would be targeted at properties themselves, not the way they are used, so it should not affect any functionality existing. Let me give a more specific example.

If you look at the current implementation of the ColorBackground class - it has one simple property:

/**
 * Background color.
 */
@Nullable
@XStreamAsAttribute
protected Color color;

There are actually a few more properties in the classes it extends, but let's ignore those for simplicity.

That color property is always specified in the style, for example like this:

<ColorBackground color="white" />

This will most probably stay as it is since first - I don't want to force anyone to redo all of their styles, and second - it would be unreasonable to overcomplicate definition of any simple properties in the XML.

But the code part is going to change completely - Color will be replaced with a custom implementation of a StyleProperty interface - probably named ColorProperty in case of Color. Each StyleProperty will handle a few things:

  1. It will contain value provided in the style (white for instance) or a reference to another property that is defined in skin variables

  2. It will contain a special behavior that will define how this property change will update component's visual representation

  3. It will also contain a method for state-related check and update in case we proceed with previously discussed variable states implementation

  4. Going forward this StyleProperty API will allow me to smoothly implement animated transitions (#355) between different values

I won't go into more details here because nothing is set in stone yet, but now I want to go back to the topic I mentioned at the start - how exactly variables will be modifiable from the code.

Modifying variables from code

Let's say variables are implemented and we have one in the skin:

    <!-- Skin variables -->
    <variables>

        <!-- Round -->
        <integer id="round.small">2</integer>

    </variables>

You will be able to adjust this globally for the whole skin:

StyleManager.set ( "round.small", 4 );
StyleManager.getSkin ().set ( "round.small", 4 );
component.getSkin ().set ( "round.small", 4 );

This will change this variable value to 4 for the current default/component skin.

There will also be another way to set variable value for specific component:

component.setSkinVariable ( "round.small", 4 );

Method name is a subject to change, but overall it should give an idea of what should become possible with such addition - you will basically be able to redefine a variable style uses from the code for any particular component.

Anything that will be available in variables - will be available for change. I'm not yet sure if variables will stay more "primitive" (color/int/insets/etc) or will have advanced constructs (such as IBackground implementations from example above in this issue) - this is something that will be decided once I get to implementing it.

The per-component variable adjustment will be implemented through a clever use of JComponent client properties. Basically each modified variable will put custom value under a specific key like "StyleProperty:{skin.id}:{property.id}" which will be detected by the particular StyleProperty and used instead of the value provided in style. This is just a basic concept, but it will most probably be implemented this way.

daWoife commented 4 years ago

I know it will be a lot of work to do this, but I think it's the most important thing to do. If you only have one light skin as you had in the 'old' weblaf, the most background parts of an application are white or gray, the foregrounds are black. This looks good an every monitor, regardless of the settings you have choosen or the resolution you have. That's different with a dark skin, so if you offer a user a dark skin, you must give him the opportunity to change colors at runtime for his personal contentment. Without that I think the most users won't switch to a dark skin even if they would like. As an example I have two monitors, on the older one your dark skin really looks good and smooth, but on my secound and newer monitor with the higher resolution it's not good, hurting my eyes after a few minutes of looking at it.

So I already added a dialog to our applikation there a user can change some colors. To do that is not too difficult for the foreground colors ..

XmlSkin skin = (XmlSkin)StyleManager.getSkin();
SkinInfo info = skin.getSkinInfo();

for (ComponentStyle style : info.getStyles()) {

    switch  (style.getPathId()) {

        case "textfield:textfield":
            style.getComponentProperties().put("foreground", new ColorUIResource(...));
            break;     
        case ...
    }
}

So it's easy to change component properties like the foreground color, but changing background colors is a bit more complicated. You have to iterate over the painter properties to change colors, and that's not a nice thing to do, and so variables here would be really a gift.

So I hope you think again about your putting this feature in a late future. It would really be nice to have it earlier. :-)

mgarin commented 4 years ago

The main point of variables is exactly opening up any simple Skin settings for runtime access and modification. So things like colors of any particular Skin will be moved into variables and reused across all styles so they can be easily retrieved and changed.

So what you're asking for is exactly the idea behind the variables.

Regarding how soon it will be available - it is already the next big thing in the line (planned for v1.4.0) after the performance improvements coming in v1.3.0. It is simply not possible to push it any earlier since it will require some of the improvements I'm working on for the v1.3.0 update. And there will surely be some minor updates - between now and v1.3.0, and between v1.3.0 and v1.4.0 - that will include bugfixes and smaller features.

Variables might also be the last major update for WebLaF v1 and all further improvements (besides bugfixes) coming in later updates will not support JDK 6 or 7 anymore as a lot of things I can potentially improve are being held back (like file chooser/IO improvements, better public APIs etc).

mgarin commented 4 years ago

As a side note - addition of new basic Skins available in WebLaF is also being held back by the absence of the variables feature (and proper fonts support), so I would like to have it as soon as possible as well.

daWoife commented 1 year ago

It still would be a very nice and big improvement of WebLaF to have this feature of skin variables. So do you think it will come anymore ?