Friday, December 11, 2009

Creating Custom Caspian Controls

Creating custom controls in JavaFX is not difficult. Creating custom controls that fit nicely with the default Caspian theme, with its pervasive use of gradients and animation, is a little trickier. This is only because the utility classes and methods that make it simple lie buried within com.sun packages that are not publicly documented. A little digging reveals a few gems that can be used to make your controls fit right in with the core JavaFX controls.
If you are observant, you will not fail to notice that this post also serves to emphasize some of the many reasons that I love, and love working with, Caspian.
Warning #1 This post is not an introduction to writing controls in JavaFX. I will not cover the basics of the Control - Skin - Behavior classes. A quick and simple tutorial on this topic can be found here.
Warning #2 All of the information in this post applies only to JavaFX 1.2.1. One of the dangers of using non-public classes is that they can change significantly from version to version (and they certainly will for JavaFX 1.3). If there is interest, I will update this information after JavaFX 1.3 is released.
Apology #1 I discovered the information I'm about to present here while writing my own controls. None of this should be considered official Caspian documentation since I do not work for Sun and I do not have access to their source code (but hopefully I will when they release the source someday). Everything here is accurate as far as I can tell, but I apologize if I have any of the details wrong. Hopefully someone from Sun will read this and let me know if I've made any mistakes.
Knock-knock Joke #1 (of 100) Knock, knock. Who's there? Yewben. Yewben who? Yewben warned, now on with the post!

Caspian 101 - Colors and Gradients

Caspian controls in JavaFX 1.2.1 all make use of a mixin class called Colorable. This mixin provides the ubiquitous base and accent properties that can be used to style the controls. The base property specifies the base color of the control while the accent property specifies the accent color on those controls that support it. ListView, for example, uses the accent property to specify the color with which it draws the selection highlight in the list. Usage of the Colorable mixin makes Caspian controls incredibly easy to style.
Two other colors are generated from the base color: the over color, which is used when the mouse pointer is over the control, and the pressed color, used when a control is (you guessed it) pressed. Caspian also generates five Paint objects from the base color: the body paint, the text paint, the border paint, the highlight line paint, and the shadow highlight paint. The screenshot below shows an application that displays these various colors and paints. It has been annotated with lines that show where they are used on a Caspian button (click the image to see a larger version).
If you launch the application, you can see that the button is a live control that has been scaled up to four times its normal size. If you hover the mouse cursor over the button or click on it, you will see the control's use of the over and pressed colors as well. Caspian is remarkably clever about generating nice looking controls from a single color.


The Caspian class in the com.sun.javafx.scene.control.caspian package provides easy access to these color-generating capabilities via module level functions:
  • Caspian.getOverColor( base: Color )
  • Caspian.getPressedColor( base: Color )
  • Caspian.getBodyPaint( base: Color )
  • Caspian.getTextPaint( base: Color )
  • Caspian.getBorderPaint( base: Color )
  • Caspian.getHighlightLinePaint( base: Color )
  • Caspian.getShadowHighlightPaint( base: Color )
Therefore, the first step in creating a control that fits in with the native Caspian controls is to use the Colorable mixin class and use it's base property to generate the other colors for your control.
public class MyControlSkin extends Skin, Colorable {

def bodyPaint = bind Caspian.getBodyPaint( base );
def overPaint = bind Caspian.getBodyPaint( Caspian.getOverColor( base ) );
def pressedPaint = bind Caspian.getBodyPaint( Caspian.getPressedColor( base ) );

var mousePressed = false;

def background = Rectangle {
   fill: bind {
       if (mousePressed) {
           pressedPaint
       } else if (background.hover) {
           overPaint
       } else {
           bodyPaint
       }
   }
   stroke: bind Caspian.getBorderPaint( base )
   blocksMouse: true
   onMousePressed: function( me: MouseEvent ) {
       mousePressed = true;
       // ... may also need to notify the Behavior ...
   }
   onMouseReleased: function( me: MouseEvent ) {
       mousePressed = false;
       // ... may also need to notify the Behavior ...
   }
}

// ... I'm too modest to show more skin ...
}
   
With just a few lines of code you get nice Caspian gradients for your control plus your custom control will respond to the same base style property as any core JavaFX control. In this simple example I only changed the body paint in response to mouse events. The core JavaFX controls also tend to update their border and highlight colors in response to hover and pressed events. You can see this when you run the Caspian Colors application above and interact with the button.

Caspian 102 - Animated State Transitions

Getting the colors and gradients right is only half the battle. Caspian also uses nice animated transitions when the control changes its state. For instance, when a button is disabled it doesn't just flip to a disabled state, it animates the transition with a smooth fade of the button's opacity. Similar animations occur when a control gets enabled, gains or loses focus, gets hovered over, or gets clicked. Caspian provides a smooth and fluid user experience.
This is something I want to support in my own controls since I am a subscriber to the Chet Haas school of thought regarding the positive role that good, subtle animation cues can play in a user interface. Animating all of those state transitions could become a real chore, so it's a good thing that you can cheat and take advantage of the hard work already done by the team at Sun. Allow me to introduce you to the States, State, and StateTransition classes. All of these little beauties are found in the com.sun.javafx.animation.transition package.
The State class allows you to define each state of your control. The class has four properties of interest:
idThe name of the state.
defaultState Should be set to true if this state is the one in which your control starts.
active Should be set to true when this state is active. Normally this will be bound to some property of the control. For example, when defining the "hover" state, you would bind the state's active property to the skin node's hover property.
priority An integer value that is used to establish precedence when multiple states are active at the same time.
The StateTransition class derives from Transition and allows you to define the animation that will occur when your control moves from one state to another. The properties of interest in this class are id, fromState, toState, and animation. The fromState property is a string that allows you to specify the id of the state you are transitioning from. Likewise for the toState property.
And finally, the States class keeps track of your control's states and manages the transitions between them. The following code shows an example of the states and transitions for a custom control.
def FAST_TRANSITION = 250ms;
def SLOW_TRANSITION = 500ms;

public class MyControlSkin extends Skin, Colorable {
override var color = base;  // color is another property defined in Colorable

def over = bind Caspian.getOverColor( base );
def pressed = bind Caspian.getPressedColor( base );

var mousePressed = false;

def states = States {
   states: [
       State { id: "disabled", active: bind control.disabled }
       State { id: "armed",    active: bind mousePressed }
       State { id: "hover",    active: bind control.hover }
       State { id: "enabled",  active: bind not control.disabled, defaultState: true }
   ]
   transitions: [
       StateTransition {
           id: "Enter-Enabled"
           toState: "enabled"
           animation: ParallelTransition {
               content: [
                   ColorTransition {
                       colorable: this
                       toValue: base
                       duration: SLOW_TRANSITION
                   }
                   FadeTransition {
                       node: bind node
                       toValue: 1.0
                       duration: FAST_TRANSITION
                   }
               ]
           }
       }
       StateTransition {
           id: "Enter-Hover"
           toState: "hover"
           animation: ColorTransition {
               colorable: this
               toValue: over
               duration: FAST_TRANSITION
           }
       }
       StateTransition {
           id: "Enter-Armed"
           toState: "armed"
           animation: ColorTransition {
               colorable: this
               toValue: pressed
               duration: FAST_TRANSITION
           }
       }
       StateTransition {
           id: "Enter-Disabled"
           toState: "disabled"
           animation: FadeTransition {
               node: bind node
               toValue: 0.33
               duration: FAST_TRANSITION
           }
       }
   ]
}

// ... Move along, nothing more to see ...
}
   
True to the spirit of JavaFX's declarative syntax, all you have to do is declare your states and the transitions between them and the rest is handled for you! You can't ask for easier animations than that.
In addition to those shown above, many controls also have "focused", "focused+hover", and "focused+armed" states defined as well (along with their corresponding transitions). Note that I did not specify any priorities in the State declarations above. That is because the order in which the states are declared establishes a default priority. You only need to specify an explicit priority if you want to override this default. Also note that I didn't specify a fromState in any of my state transitions. I was telling the States class that it can use that transition when entering the target state from any other state. You only need to specify a fromState if you want the transition to be used only when entering a state from one other particular state.
This code makes use of another Caspian gem, the ColorTransition class which is found in the com.sun.javafx.scene.control.caspian package. It will animate the Colorable mixin's color property from one value to another. This is yet another reason to make use of the Colorable mixin in your code - easy color animations!

The Etched Button Control

This control was created because I wanted a close button for the XPane control that looked like it was etched into the background of the title bar. I wanted the etched button to fit in as closely as possible with the other Caspian controls as well. The resulting control is shown in the image below.

The button features animations for over and pressed states as well as the disabled state. There are a few other nice features as well. The button can display text or a graphic, or both. It can optionally apply an etched effect to its content. The effect is similar to the one Jasper shows in a recent blog post.
Clicking on the disable check box will disable both the etched button and the normal Caspian button. You can also compare their over and pressed animations to verify that they are a close match. This control shows that it is possible to match the look and feel of the Caspian controls very closely by simply using some of the classes that are provided with the standard JavaFX runtime.

P.S.

Knock, knock. Who's there? Yewer. Yewer who? Yewer afraid I was going to tell 100 knock-knock jokes weren't you!

8 comments:

  1. Dean, a great post :-) One thing to note is the private Caspian class will be going in the next release of JavaFX but you will be able to do everything you are doing here in a much simpler way in CSS. So we are not reducing but building on this idea to make theming really simple.

    ReplyDelete
  2. Thanks for the heads up, Jasper. I'll post an update when 1.3 comes out.

    ReplyDelete
  3. Great +1!
    I do not think I will use non-public classes but
    I wish we had a better documentation for the colors, gradients used in the caspian theme.
    Jasper did a great job but it is quite difficult to reproduce for me as a developer ;-)

    ReplyDelete
  4. i am able to build the charts which you have shown.But i am not able to build the background of the charts(i.e gridlines) which you have shown.

    I am searching for the solution in google like a mad.can you please help me in this.

    thanks in advance

    ReplyDelete
  5. uday,

    I didn't do anything special to the chart backgrounds. All of the source code for those samples is available here:

    http://jfxtras.org/portal/pro-javafx-platform

    Just scroll down and look under the Chapter 5 samples.

    Dean

    ReplyDelete
  6. Can you provide a code for these applications, cause I can't implement states :(

    ReplyDelete
  7. The XEtchedButton from this post is part of JFXtras. Check out its skin class:

    http://code.google.com/p/jfxtras/source/browse/jfxtras.core/trunk/controls/src/org/jfxtras/scene/control/XEtchedButtonSkin.fx?r=665

    Dean

    ReplyDelete
  8. Thank you Dean. Actually I was with this code, but I couldn't implement it myself. Trying to find out where is my problem.

    P.S. Thanks for the article, really helpful. Thanx a lot.

    ReplyDelete

Please Note: All comments are moderated. That's why you won't see your comment appear right away. If it's not some stupid piece of spam, it will appear soon.

Note: Only a member of this blog may post a comment.