Showing posts with label JavaFX. Show all posts
Showing posts with label JavaFX. Show all posts

Wednesday, 11 March 2015

JavaFX Filter ComboBox

Quite a lot of posts exist on the web about how hard it is to do a filter / filtered / filtering combo box in JavaFX.  Using one of these as a basis here is an example of what to do.  The key part is creating a StringConverter so that the strings that are displayed in the ComboBox can be translated to and from the underlying objects.

FilterComboBox

This class is a extension of the standard JavaFX ComboBox. The list of initialItems needs to be populated either through the constructor or by using the setInitialItems() method so that there is a definitive list to go back to when necessary.

import java.util.ArrayList;

/**
 * A control which provides a filtered combo box.  As the user
 * enters values into the combo editor the list is filtered automatically.
 *
 * @param <T> the object type that is held by this FilteredComboBox
 */
public class FilterComboBox<T extends Object> extends ComboBox<T>
{
    /**
     * The default / initial list that is in the combo when nothing
     * is entered in the editor.
     */
    private Collection<T> initialList = new ArrayList<>();

    /**
     * Check type.  True if this is startsWith, false if it is contains.
     */
    private final boolean startsWithCheck;

    /**
     * Constructs a new FilterComboBox with the given parameters.
     *
     * @param startsWithCheck true if this is a 'startsWith' check false if it is 'contains' check
     */
    public FilterComboBox(final boolean startsWithCheck)
    {
        super();
        this.startsWithCheck = startsWithCheck;

        super.setEditable(true);

        this.configAutoFilterListener();
    }

    /**
     * Constructs a new FilterComboBox with the given parameters.
     *
     * @param items The initial items
     * @param startsWithCheck true if this is a 'startsWith' check false if it is 'contains' check
     */
    public FilterComboBox(final ObservableList<T> items, final boolean startsWithCheck)
    {
        super(items);
        this.startsWithCheck = startsWithCheck;
        super.setEditable(true);
        initialList = items;

        this.configAutoFilterListener();
    }

    /**
     * Set the initial list of items into this combo box.
     *
     * @param initial The initial list
     */
    public void setInitialItems(final Collection<T> initial)
    {
        super.getItems().clear();
        super.getItems().addAll(initial);
        this.initialList = initial;
    }

    /**
     * Set up the auto filter on the combo.
     */
    private void configAutoFilterListener()
    {
        this.getEditor().textProperty().addListener(new ChangeListener<String>()
        {
            @Override
            public void changed(final ObservableValue<? extends String> observable, final String oldValue, final String newValue)
            {
                final T selected = getSelectionModel().getSelectedItem();
                if (selected == null
                        || !getConverter().toString(selected).equals(getEditor().getText()))
                {
                    filterItems(newValue);

                    if (getItems().size() == 1)
                    {
                        setUserInputToOnlyOption();
                        hide();
                    }
                    else if (!getItems().isEmpty())
                    {
                        show();
                    }
                }
            }
        });
    }

    /**
     * Method to filter the items and update the combo.
     *
     * @param filter The filter string to use.
     */
    private void filterItems(final String filter)
    {
        final ObservableList<T> filteredList = FXCollections.observableArrayList();
        for (T item : initialList)
        {
            if (startsWithCheck && getConverter().toString(item).toLowerCase().startsWith(filter.toLowerCase()))
            {
                filteredList.add(item);
            }
            else if (!startsWithCheck && getConverter().toString(item).toLowerCase().contains(filter.toLowerCase()))
            {
                filteredList.add(item);
            }
        }

        setItems(filteredList);
    }

    /**
     * If there is only one item left in the combo then we assume this is correct.
     * Put the item into the editor but select the end of the string that the user
     * hasn't actually entered.
     */
    private void setUserInputToOnlyOption()
    {
        final String onlyOption = getConverter().toString(getItems().get(0));
        final String currentText = getEditor().getText();
        if (onlyOption.length() > currentText.length())
        {
            getEditor().setText(onlyOption);
            Platform.runLater(new Runnable()
            {
                @Override
                public void run()
                {
                    getEditor().selectAll();
                }
            });
        }
    }
}

StringConverter

The key to this working correctly is a StringConverter object which allows JavaFX to convert properly from the Object in the ComboBox to the String which is displayed in the ComboBox.  The StringConverter is a standard JavaFX object which has a toString() and fromString() method.  The easiest way to get this working is to construct a StringConverter and provide the same list of items to it that is provided to the ComboBox.


public class MyObjectStringConverter extends StringConverter<MyObject> 
{
    /** The list of objects to do the conversions with. */
    private List<MyObject> myObjList;

    /**
     * Construct this object with the list for converting.
     *
     * @param items The items list
     *
     */
    public MyStringConverter(List<MyObject> items)
    {
        this.myObjList = items;
    }

    @Override
    public String toString(final MyObject myObj)
    {
        if (myObj != null)
        {
            return myObj.getName();
        }
        return null;
    }

    @Override
    public MyObject fromString(final String item)
    {
        for (MyObject myObj : myObjList)
        {
            if (myObj.getName().equals(item))
            {
                return myObj;
            }
        }
        return null;
    }
}

Usage

The usage of this combo is extremely easy.  Other than the standard ComboBox options in JavaFX the only things really necessary are making sure the initialList is set through the constructor or directly and making sure that the converter is used.

    final List<MyObject> myObjs = new ArrayList<>();
    ...

    final MyObjectStringConverter converter = new MyObjectStringConverter(myObjs);

    final FilterComboBox comboBox = new FilterComboBox(true);
    comboBox.setInitialItems(myObjs);
    comboBox.setConverter(converter);

Thursday, 19 June 2014

Dynamically Updating JavaFX Table

There are many tricks on the web for updating tables when a value changes and the table doesn't update such as adding and removing columns to force the table to refresh.  I think there may be a refresh table option in Java 8 now.  However, getting the data structure correct makes a big difference.

Using Properties
Use properties in the backing object so that the table can bind to them and get the updates.  In the example below the name is immutable so doesn't need a property but the count is changeable so use a property value and importantly include the countProperty() method.


public class MyTableData
{
    /**
     * A value that doesn't change so just stored as a string.
     */
    private final String name;
        
    /**
     * A changeable value so stored as a property.
     */
    private final SimpleIntegerProperty count;
    
    /**
     * Constructs a new MyTableData with the given parameters.
     *
     * @param name The name
     * @param count The count
     */
    public MyTableData (final String name, final Integer count)
    {
        this.name= name;
        this.count = new SimpleIntegerProperty(count);
    }

    /**
     * Gets the namevalue.
     *
     * @return the name
     */
    public String getName()
    {
        return name;
    }

    /**
     * Gets the count value.
     *
     * @return the count
     */
    public int getCount()
    {
        return count.get();
    }

    /**
     * Set the count value.
     *     * @param count The new value to set
     */
    public void setCount(final int count)
    {
        this.count.set(count);

    }

    /**
     * The count property.
     *
     * @return The count property
     */
    public IntegerProperty countProperty() 
    {
        return count;
    }
}

The Table
In the table create the columns as normal,

        // Count Column
        final TableColumn<MyTableData, Number> countCol = new TableColumn<MyTableData, Number>("Count");
        countCol.setCellValueFactory(new PropertyValueFactory<MyTableData, Number>("count"));


Thursday, 16 May 2013

Motion JPG / MJPG

MJPG is a format which is used by some webcams.  This format is just a series of JPG images as bytes separated by a boundary and some headers.  For example

    --myboundary
    Content-Type: image/jpg
    Content-Length: 1234

    afhaoiughwer< 1234 bytes >
    --myboundary

    Content-Type: image/jpg
    Content-Length: 2345

    poipoijngiuh< 2345 bytes >

The blank line after the headers is also used in http requests to separate headers from content - this is pretty standard. This is pretty simple to parse and get the jpg files from.  Below is a sample java class which does this.  This allows a number of images to be skipped so that not every image has to be processed if that isn't required.  This currently just bins the headers but could easily be extended to return the header properties as well.


/**
 * Class to parse a MJPG stream and generate jpg files
 */
public class MjpgParser
{
    /**
     * The content length header.
     */
    private static final String CONTENT_LENGTH_HEADER = "Content-Length: ";
    
    /**
     * The size of the content length string.  Used to chop of the head name and calculate the integer.
     */
    private static final int CONTENT_LENGTH_HEADER_SIZE = CONTENT_LENGTH_HEADER.length();

    /**
     * The input stream.
     */
    private BufferedInputStream mjpgStream;
    
    /**
     * The number of images to skip.
     */
    private int imagesToSkip;

    /**
     * Constructs a new MjpgParser with the given parameters.
     *
     * @param urlResource The url mjpg resource
     * @throws IOException an exception from opening the url
     */
    public MjpgParser(final String urlResource) throws IOException
    {
        this(urlResource, 0);
    }

    /**
     * Constructs a new MjpgParser with the given parameters.
     *
     * @param urlResource The url mjpg resource
     * @param imagesToSkip The number of images to skip.
     * @throws IOException an exception from opening the url
     */
    public MjpgParser(final String urlResource, final int imagesToSkip) throws IOException
    {
        final URL url = new URL(urlResource);
        initialise(url.openStream(), imagesToSkip);
    }
    
    /**
     * Initialise.
     * 
     * @param inputStream The Input Stream
     * @param imagesToSkipNo The number of images to skip.
     */
    private void initialise(final InputStream inputStream, final int imagesToSkipNo)
    {
        this.mjpgStream = new BufferedInputStream(inputStream);
        this.imagesToSkip = imagesToSkipNo;
    }
    
    /**
     * Get the next jpg as a JavaFX Image.
     *
     * @return an Image object
     */
    public Image nextAsImage() throws IOException
    {
        return new Image(new ByteArrayInputStream(nextWithSkip()));
    }
    
    /**
     * Get the next jpg as an array of bytes.
     *
     * @return the bytes of the next jpg
     */
    public byte[] nextAsBytes() throws IOException
    {
        return nextWithSkip();
    }
    
    /**
     * Close the streams.
     */
    public void close() throws IOException
    {
        mjpgStream.close();
    }
    
    /**
     * This method calls next but takes account of the imagesToSkip value.
     * 
     * @return the jpg file after skipping the correct number
     * @throws IOException Thrown by reading from the stream
     */
    private byte[] nextWithSkip() throws IOException
    {
        for (int i = 0; i < imagesToSkip - 1; i++)
        {
            next();
        }
        return next();
    }
    
    /**
     * Get the next jpg as bytes.  This reads the headers and then reads the jpg from the 
     * stream using the Content-Length value to know when to stop.
     * 
     * @return the next jpg file
     * @throws IOException 
     */
    private byte[] next() throws IOException
    {
        // Find the boundary line
        String lineStr = readHeaderLine();
        while (!lineStr.startsWith("--"))
        {
            lineStr = readHeaderLine();
        }
        
        // Read the headers.
        int contentLength = 0;
        lineStr = readHeaderLine();
        while (!lineStr.isEmpty())
        {
            // If this is the content length then process it.
            if (lineStr.startsWith(CONTENT_LENGTH_HEADER))
            {
                contentLength = parseContentLength(lineStr);
            }

            lineStr = readHeaderLine();
        }
        
        return readJpgBytes(contentLength);
    }
    
    /**
     * Read a line from the input stream. This is a header line so it will be a readable string 
     * and can be trimmed to remove the end of line characters.
     * 
     * @return the line as bytes
     * @throws IOException 
     */
    private String readHeaderLine() throws IOException
    {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int entry = mjpgStream.read();
        while (entry != '\n')
        {
            baos.write(entry);
            entry = mjpgStream.read();
        }
        return new String(baos.toByteArray()).trim();
    }
    
    /**
     * Parse the content length line.
     *
     * @param contentLengthLine The line to parse
     * @return The content length
     */
    private int parseContentLength(final String contentLengthLine)
    {
        return Integer.parseInt(contentLengthLine.substring(CONTENT_LENGTH_HEADER_SIZE));
    }
    
    /**
     * Read the jpg.
     * 
     * @param contentLength the length of the jpg
     * @return the jpg as an {@link Image}
     * @throws IOException 
     */
    private byte[] readJpgBytes(final int contentLength) throws IOException
    {
        final byte [] jpgBytes = new byte[contentLength];
        for (int i = 0; i < contentLength; i++)
        {
            jpgBytes[i] = (byte) mjpgStream.read();
        }
        return jpgBytes;
    }
}






Thursday, 18 April 2013

JavaFX JUnit Testing

Unit testing in JavaFX is important but normal JUnit tests will fail because they are not run on the JavaFX thread.  Below is an example of how to solve this. The two classes below take care of loading JavaFX and a JUnit test runner which guarantees that JavaFX is running and makes sure that all tests are run in the JavaFX thread.

This solution is based on a JUnit4 Runner.  The new runner class extends the BlockJUnit4ClassRunner which is the default runner for JUnit tests.  If you also need the spring support then just extend the SpringJUnit4ClassRunner instead. Both runners can co-exist if necessary because they both use the JavaFxJUnit4Application class to guarantee a single JavaFx instance.

JavaFxJUnit4ClassRunner

Runs all the unit tests in the JavaFX Thread.

import java.util.concurrent.CountDownLatch;

import javafx.application.Platform;

import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

/**
 * This basic class runner ensures that JavaFx is running and then wraps all the runChild() calls 
 * in a Platform.runLater().  runChild() is called for each test that is run.  By wrapping each call
 *  in the Platform.runLater() this ensures that the request is executed on the JavaFx thread.
 */
public class JavaFxJUnit4ClassRunner extends BlockJUnit4ClassRunner
{
    /**
     * Constructs a new JavaFxJUnit4ClassRunner with the given parameters.
     * 
     * @param clazz The class that is to be run with this Runner
     * @throws InitializationError Thrown by the BlockJUnit4ClassRunner in the super()
     */
    public JavaFxJUnit4ClassRunner(final Class<?> clazz) throws InitializationError
    {
        super(clazz);
        
        JavaFxJUnit4Application.startJavaFx();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void runChild(final FrameworkMethod method, final RunNotifier notifier)
    {
        // Create a latch which is only removed after the super runChild() method
        // has been implemented.
        final CountDownLatch latch = new CountDownLatch(1);
        Platform.runLater(new Runnable()
        {
            @Override
            public void run()
            {
                // Call super to actually do the work
                JavaFxJUnit4ClassRunner.super.runChild(method, notifier);
                
                // Decrement the latch which will now proceed.
                latch.countDown();
            }
        });
        try
        {
            latch.await();
        }
        catch (InterruptedException e)
        {
            // Waiting for the latch was interruped
            e.printStackTrace();
        }
    }
}


JavaFxJUnit4Application

Manages starting JavaFX.  Uses a static flag to make sure that FX is only started once.

Manages starting JavaFX.  Uses a static flag to make sure that FX is only started once and a LOCK to make sure that multiple calls will pause if the JavaFX thread is in the process of being started.  This improves on the previous version because it doesn't involve a sleep.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.stage.Stage;

/**
 * This is the application which starts JavaFx.  It is controlled through the startJavaFx() method.
 */
public class JavaFxJUnit4Application extends Application
{

    /** The lock that guarantees that only one JavaFX thread will be started. */
    private static final ReentrantLock LOCK = new ReentrantLock();

    /** Started flag. */
    private static AtomicBoolean started = new AtomicBoolean();

    /**
     * Start JavaFx.
     */
    public static void startJavaFx()
    {
        try
        {
            // Lock or wait.  This gives another call to this method time to finish
            // and release the lock before another one has a go
            LOCK.lock();

            if (!started.get())
            {
                // start the JavaFX application
                final ExecutorService executor = Executors.newSingleThreadExecutor();
                executor.execute(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        JavaFxJUnit4Application.launch();
                    }
                });

                while (!started.get())
                {
                    Thread.yield();
                }
            }
        }
        finally
        {
            LOCK.unlock();
        }
    }

    /**
     * Launch.
     */
    protected static void launch()
    {
        Application.launch();
    }

    /**
     * An empty start method.
     *
     * @param stage The stage
     */
    @Override
    public void start(final Stage stage)
    {
        started.set(Boolean.TRUE);
    }
}

Sample Test

A sample test class which instantiates a Scene object.  This will fail if it isn't on the JavaFX thread.  It can be shown to fail if the @RunWith annotation is removed.


/**
 * This is a sample test class for java fx tests.
 */
@RunWith(JavaFxJUnit4ClassRunner.class)
public class ApplicationTestBase
{
    /**
     * Daft normal test.
     */
    @Test
    public void testNormal()
    {
        assertTrue(true);
    }

    /**
     * Test which would normally fail without running on the JavaFX thread.
     */
    @Test
    public void testNeedsJavaFX()
    {
        Scene scene = new Scene(new Group());
        assertTrue(true);
    }
}


JavaFxJUnit4Application - Previous version

Manages starting JavaFX.  Uses a static flag to make sure that FX is only started once.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.stage.Stage;

/**
 * This is the application which starts JavaFx.  It is controlled through the startJavaFx() method.
 */
public class JavaFxJUnit4Application extends Application
{
    /**
     * Flag stating if javafx has started. Static so that it
     * is shared across all instances.
     */
    private static boolean started;
    
    /**
     * Start JavaFx.
     */
    static void startJavaFx()
    {
        if (started)
        {
            return;
        }
        
        started = true;
        
        /**
         * The executor which starts JavaFx.
         */
        final ExecutorService executor = Executors.newSingleThreadExecutor();

        // Start the java fx application
        executor.execute(new Runnable()
        {
            @Override
            public void run()
            {
                JavaFxJUnit4Application.launch();
            }
        });

        // Pause briefly to give FX a chance to start
        try
        {
            Thread.sleep(1000);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    /**
     * Launch.
     */
    static void launch()
    {
        Application.launch();
    }

    /**
     * An empty start method.
     * 
     * @param stage The stage
     */
    @Override
    public final void start(final Stage stage)
    {
        // Empty
    }
}

Friday, 15 February 2013

JavaFX Pan & Zoom

These notes are based around putting objects into a normal Pane object and not, for example, a ScrollPane where the rules for finding the centre point to pivot a scale around are a bit odd!

JavaFX gives amazing controls over panning and zooming.  For example to pan a pane you can simply use

    pane.setTranslateX(10);
    pane.setTranslateY(10);

This will set the translate.  If you want to move an object more from its currently panned position use

    pane.setTranslateX(pane.getTranslateX() + ...);
    pane.setTranslateY(pane.getTranslateY() + ...);

Zooming is very similar, to double the size around the centre point of the pane,

    pane.setScaleX(2);
    pane.setScaleY(2);

Putting these together is fine and works well.  However, you would normally want to zoom around the centre point of the screen rather than the centre point of the pane, particularly if the pane has been panned first. To do a zoom around a particular point you can use the Scale transformation provided as standard.

    Scale scale = new Scale();
    scale.setPivotX(pivotX);
    scale.setPivotY(pivotY);
    scale.setX(zoomFactorX);
    scale.setY(zoomFactorY);
    pane.getTransforms().add(scale);

The pivotX, pivotY point here is the point around which the scale takes place.  How, the problem comes to calculate the pivot point.  You can do this fairly easily using the position of the pane in the parent.

    Bounds bip = pane.boundsInParent().get();
    double pivotX = (screenWidth / 2 - bip.getMinX()) / scaleX;
    double pivotY = (screenHeight / 2 - bip.getMinY()) / scaleY;

    Scale scale = new Scale()
    ...

    scaleX *= zoomFactorX;
    scaleY *= zoomFactorY;

Where scaleX and scaleY are the current scaling.  You can keep track of the current scale easily enough by adjusting it after each zoom (as above) or you can get the current width / height from the bounds object and calculate the zoom from the original size.

For consistency, you can now use a transform to do the translation as well.  This makes no difference to the scaling and pivot calculation as the boundsInParent will still be the same whichever way the translation is done.

    Translate translate = new Translate(panX, panY);
    pane.getTransforms().add(translate);

JavaFX Layout Sizes

If you are using JavaFX then it is worth knowing that the definitive way to get size and position information for a Pane is to use

    Bounds bounds = pane.boundsInParent().get();

The Bounds object obtained gives width, height and translated position (minX, minY) for this pane in its parent.  Even if the width of a pane is set it may get overridden by the layout if there is insufficient room.  Therefore, you cannot rely on what you think the size is.  Instead use the bounds above to get the actual answer.