Difference between revisions of "XanderPaintingFramework"
m (Add initial details to an Introduction section) |
m (change a couple of section titles that I didn't like) |
||
(13 intermediate revisions by the same user not shown) | |||
Line 9: | Line 9: | ||
[[File:xander-painter-framework.png]] | [[File:xander-painter-framework.png]] | ||
− | If you have looked at or are aware of the [[XanderFramework|Xander Robot Framework]], note that the Xander Painting Framework stands on it's own. While the Xander Robot Framework makes use of the Xander Painting Framework, the Xander Painting Framework does not require the Xander Robot Framework. | + | ''Note: If you have looked at or are aware of the [[XanderFramework|Xander Robot Framework]], note that the Xander Painting Framework stands on it's own. While the Xander Robot Framework makes use of the Xander Painting Framework, the Xander Painting Framework does not require the Xander Robot Framework.'' |
+ | |||
+ | = Version History = | ||
+ | |||
+ | == Version 1.1 == | ||
+ | |||
+ | * Fixed bug in where resize handle appears on windows (this bug was most evident when restoring windows between battles). | ||
+ | |||
+ | == Version 1.0 == | ||
+ | |||
+ | Initial release. | ||
= Paintables and the Paintable Interface = | = Paintables and the Paintable Interface = | ||
Line 36: | Line 46: | ||
There are two types of paintable objects: paintable objects with specific painters, and paintable objects without. In the former case, the getPainterName() method should return the name of the specific painter responsible for painting the specific paintable instance. In the latter case, the getPainterName() method should return null and any Painter can paint information from a single instance (and only a single instance) of the class. | There are two types of paintable objects: paintable objects with specific painters, and paintable objects without. In the former case, the getPainterName() method should return the name of the specific painter responsible for painting the specific paintable instance. In the latter case, the getPainterName() method should return null and any Painter can paint information from a single instance (and only a single instance) of the class. | ||
− | = The Painter Interface = | + | = Painting = |
+ | |||
+ | Probably the best way to demonstrate how to use the framework for painting, and to demonstrate how what might sound like a complicated framework makes life easier is to provide an example. Below is the source code for a Painter class that prints the name of the current drive, gun, and radar to a window on the screen (this Painter is part of the Xander Robot Framework; the RobotProxy class is a Paintable class that provides access to this information). | ||
+ | |||
+ | <pre> | ||
+ | /** | ||
+ | * Paints the name of the currently active radar, drive, and gun to the screen. | ||
+ | * | ||
+ | * @author Scott Arnold | ||
+ | */ | ||
+ | public class ActiveComponentsPainter extends TextPainter<RobotProxy> { | ||
+ | |||
+ | private StringBuilder sb = new StringBuilder(); | ||
+ | |||
+ | public ActiveComponentsPainter() { | ||
+ | super("Active Components", 200, 3); // default size: width 200, height 3 lines of text | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Class<RobotProxy> getPaintableClass() { | ||
+ | return RobotProxy.class; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | protected String getText(RobotProxy robotProxy) { | ||
+ | sb.setLength(0); | ||
+ | sb.append("Radar: ").append(robotProxy.getActiveRadarName()).append("\n"); | ||
+ | sb.append("Drive: ").append(robotProxy.getActiveDriveName()).append("\n"); | ||
+ | sb.append("Gun: ").append(robotProxy.getActiveGunName()); | ||
+ | return sb.toString(); | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | The above example makes use of the framework's TextPainter class. The framework providers the following painting interface and classes: | ||
+ | * Painter -- root interface for painters within the framework | ||
+ | * AbstractPainter -- abstract painter class that adds support for togging painters on and off through the menu | ||
+ | * WindowPainter -- abstract painter class that extends the AbstractPainter class and provides a window to paint within. | ||
+ | * TextPainter -- abstract painter class that extends the WindowPainter class and is designed for text-only displays. | ||
+ | |||
+ | == The Painter Interface == | ||
Implementing the '''Painter''' interface is the minimum required to be painted by the framework. While you can create a class that implements Painter, it is better to extend one of the framework abstract classes. Otherwise, the Painter will still appear in the menu but you will not be able to toggle the painter on and off. | Implementing the '''Painter''' interface is the minimum required to be painted by the framework. While you can create a class that implements Painter, it is better to extend one of the framework abstract classes. Otherwise, the Painter will still appear in the menu but you will not be able to toggle the painter on and off. | ||
− | = The AbstractPainter Class = | + | == The AbstractPainter Class == |
The '''AbstractPainter''' class adds support for toggling the painter on and off. Anything that extends AbstractPainter or one of it's subclasses will have a toggle button for it in the menu. | The '''AbstractPainter''' class adds support for toggling the painter on and off. Anything that extends AbstractPainter or one of it's subclasses will have a toggle button for it in the menu. | ||
− | + | Extend this class for painters that wish to paint over the entire battle field, such as painters that paint bullet waves. | |
− | = The WindowPainter Class = | + | == The WindowPainter Class == |
− | The '''WindowPainter''' class provides a interactive window that the painter can paint within. When using this class, position (0, 0) is the lower left hand corner of the window. Windows can be moved around the screen by dragging them by their title bars. Not all Graphics2D painting methods are supported when using a WindowPainter; only draw(Shape) and the various drawString(...) methods are supported. | + | The '''WindowPainter''' class provides a interactive window that the painter can paint within. When using this class, position (0, 0) is the lower left hand corner of the window. Windows can be moved around the screen by dragging them by their title bars, and resized by dragging the resize handle at the bottom right of the window. Not all Graphics2D painting methods are supported when using a WindowPainter; only draw(Shape) and the various drawString(...) methods are supported. |
== The TextPainter Class == | == The TextPainter Class == | ||
Line 85: | Line 135: | ||
/** | /** | ||
* PaintManager to use with the Xander robot framework. If using the Xander | * PaintManager to use with the Xander robot framework. If using the Xander | ||
− | * robot framework, make sure you call | + | * robot framework, make sure you call the getInstance() methods on this class, |
* and not on the regular PaintManager class. | * and not on the regular PaintManager class. | ||
* | * | ||
Line 93: | Line 143: | ||
private XanderPaintManager() { | private XanderPaintManager() { | ||
− | // | + | // RobotEvents is a Xander Robot Framework class that manages events. |
// It contains all the event listeners needed to ensure all the proper | // It contains all the event listeners needed to ensure all the proper | ||
// PaintManager methods get called. | // PaintManager methods get called. | ||
Line 135: | Line 185: | ||
</pre> | </pre> | ||
− | This example from the Xander Robot Framework also overrides the primary enable(...) method, and adds another custom enable(...) method. This is not something that needs to be done; the Xander Robot Framework does this to ensure it's framework painters get added to the PaintManager in addition to any custom painters the robot adds. | + | This example from the Xander Robot Framework also overrides the primary enable(...) method, and adds another custom enable(...) method. This is not something that needs to be done; the Xander Robot Framework does this to ensure it's own framework painters get added to the PaintManager in addition to any custom painters the robot adds. |
+ | |||
+ | = Saving and Restoring Painter Settings = | ||
+ | |||
+ | Optionally, you can choose to save the state of the painting framework between battles. This allows all of your previously enabled painters, colors, windows sizes, and window locations to be as you left them the next time you use the painting framework. | ||
+ | |||
+ | To use this functionality, make sure the following methods gets called: | ||
+ | * public void restoreState(AdvancedRobot robot) -- call this method after enabling the framework. | ||
+ | * public void saveState(AdvancedRobot robot) -- call this method at the end of the battle. | ||
+ | |||
+ | ''Note: Don't forget to also comment out the call to these methods as well (or have your custom PaintManager subclass handle it, if you have one) when disabling the framework to reduce the packaged JAR size.'' | ||
+ | |||
+ | Updating your robot version does not clear out the saved painting framework information. If your robot has changed significantly and you want to clear out previously saved window information, you can either ''not'' call restoreState(...), or at any time before calling saveState(...), you can call clearState(). | ||
+ | |||
+ | Window information is keyed by the Painter names, so if you change a painter name, the state will not be restored. | ||
= Orphan Painters and Paintables = | = Orphan Painters and Paintables = | ||
Line 147: | Line 211: | ||
= Errors = | = Errors = | ||
− | If you have a Paintable that specifies a Painter name, and the corresponding Painter of that name specifies a Paintable class that is not compatible with the Paintable class, an IllegalArgumentException will be thrown. | + | If you have a Paintable that specifies a Painter name, and the corresponding Painter of that name specifies a Paintable class that is not compatible with the actual Paintable class, an IllegalArgumentException will be thrown. |
+ | |||
+ | = Odds and Ends = | ||
+ | |||
+ | Part of the difficulty in developing this framework is integrating it into the limited painting environment that Robocode provides. Some Java painting capabilities are not allowed by Robocode, and some just don't work. On top of that, there are no built-in event listeners for Robocode events nor mouse events. Below is a list of these issues that had to be overcome, how they were overcome, and tips to handling the remaining oddities. | ||
+ | |||
+ | == Using Transforms in Robocode == | ||
+ | |||
+ | Creating a window that can be moved around the battle field was easier said than done. In order to support movable windows, there needs to be a way to shift the coordinate system. Normally, this could be done by manipulating the Transform associated with the Graphics2D object. However, in the unique Robocode evironment, this is not possible. | ||
+ | |||
+ | Early on, one idea for handling this situation was to create a separate image, create a separate Graphics2D just for that image, and then paint the image onto the main Robocode Graphics2D object. However, Robocode security does not allow you to call the drawImage methods, so this idea was out. | ||
+ | |||
+ | The final solution was to build a proxy class to the Robocode Graphics2D object. The proxy class has some of the same paint methods defined in it as Graphics2D, but before delegating actual painting to the Graphics2D object, the Shape or text position is translated to provide the proper on screen location. Text is as simple as changing the (x,y) coordinate of where the text will be drawn. For Shapes, a Transform is applied to the Shape itself, rather than to the Graphics2D object. | ||
+ | |||
+ | == Clipping in Robocode == | ||
+ | |||
+ | Another problem with the window system was how to clip the painting such that a window painter cannot paint outside of the window bounds. Much like Transforms, using the standard Graphics2D clipping functions is not an option, as they either don't work or cause exceptions in the Robocode environment. | ||
+ | |||
+ | The solution for clipping Shapes was to use a set of classes specifically designed for clipping Shapes into rectangles. It does this be creating a new GeneralPath for the Shape where all points are forced to remain within the clipping Rectangle. This [http://javagraphics.blogspot.com/2007/04/shapes-clipping-to-rectangle.html Clipping to a Rectangle solution] is credited to Jeremy Wood, and is covered by a modified BSD license, Copyright (c) 2011, Jeremy Wood. A copy of the BSD license conditions is included in the Javadoc for the Clipper class in Clipper.java. | ||
+ | |||
+ | For clipping text, the FontMetrics instance of the Graphics2D object is used to determine how much of the text will fit within the window bounds. Text that is too long is truncated and an ellipsis is added to the end. Text completely off the screen or text that begins off screen is not printed. | ||
+ | |||
+ | drawString methods utilizing the AttributedCharacterIterator are not clipped. | ||
+ | |||
+ | == No Built-In Event Handlers == | ||
+ | |||
+ | Another problem with developing this framework was in handling the events, as there are no built-in listeners for a Robocode robot. As such, the user of this framework must ensure that the proper event methods get called. Much of this is detailed in the sections on enabling and disabling the framework (see the appropriate section above). However, a few other minor related details that can help make this framework work more smoothly include: | ||
+ | |||
+ | * Between rounds, mouse events can get lost. This can cause strange behavior if you are interacting with the framework between rounds; for example, if a round ends while dragging a window, upon start of the next round the window can continue to drag even if you are no longer holding the mouse button down. To avoid this problem, if you have extended the PaintManager class, simply set the PaintManager variables ''dragTarget'' and ''resizeTarget'' to null between rounds (they are declared ''protected'' to allow for this). Regardless of whether or not you extended PaintManager, you can also just call ''mouseReleased(null)'' on the PaintManager instance between rounds, as this accomplishes the same thing (however, this is not guaranteed to continue to work properly with future updates to the famework). | ||
+ | |||
+ | = ToDo's = | ||
+ | |||
+ | The following features remain to be implemented: | ||
+ | # Add close button on the window title bars. | ||
+ | # Add ability to change more of the colors and styles used in the framework. | ||
+ | # Improve default color palette. | ||
+ | # Change toggle button enabled images from squares to check marks to make their behavior more obvious. | ||
+ | |||
+ | = Obtaining a Copy = | ||
+ | |||
+ | Version 1.0 of the [http://www.distantvisions.net/robocode/Xpf.zip Xander Painting Framework source code] is available as a ZIP file. In addition to the main painting framework classes in the xander.paint package, it also includes the Xander file utility class in the xander.util package, and the Clipper classes in the com.bric package, both of which are required. |
Latest revision as of 06:42, 27 February 2013
Contents
Introduction
The Xander Painting Framework is a framework to aid in painting information to the screen in Robocode. It's primary goals are to:
- Provide a convenient means for toggling painters on and off.
- Provide a construct for painting informational displays in windows that can be moved around the screen.
- Make it easy to remove most of the painting components from finished robots to reduce code size, and make it equally easy to add them back in for development and debugging.
Xander Painting Framework Screen shot (showing Painters of the Xander Robot Framework):
Note: If you have looked at or are aware of the Xander Robot Framework, note that the Xander Painting Framework stands on it's own. While the Xander Robot Framework makes use of the Xander Painting Framework, the Xander Painting Framework does not require the Xander Robot Framework.
Version History
Version 1.1
- Fixed bug in where resize handle appears on windows (this bug was most evident when restoring windows between battles).
Version 1.0
Initial release.
Paintables and the Paintable Interface
The first step towards using the Xander Painting Framework is to make some information paintable. Doing this requires two steps:
- Implementing the Paintable interface on a class that will provide information to be painted.
- Adding the Paintable object to the Paintables class.
Any code this is necessary to make an object paintable will remain with the robot whether the framework is enabled or disabled, so it is best to keep such code to a minimum if possible.
The Paintable interface:
public interface Paintable { /** * Returns a unique name for the Painter that is responsible for painting * this Paintable. * * @return Painter name for this Paintable */ public String getPainterName(); }
There are two types of paintable objects: paintable objects with specific painters, and paintable objects without. In the former case, the getPainterName() method should return the name of the specific painter responsible for painting the specific paintable instance. In the latter case, the getPainterName() method should return null and any Painter can paint information from a single instance (and only a single instance) of the class.
Painting
Probably the best way to demonstrate how to use the framework for painting, and to demonstrate how what might sound like a complicated framework makes life easier is to provide an example. Below is the source code for a Painter class that prints the name of the current drive, gun, and radar to a window on the screen (this Painter is part of the Xander Robot Framework; the RobotProxy class is a Paintable class that provides access to this information).
/** * Paints the name of the currently active radar, drive, and gun to the screen. * * @author Scott Arnold */ public class ActiveComponentsPainter extends TextPainter<RobotProxy> { private StringBuilder sb = new StringBuilder(); public ActiveComponentsPainter() { super("Active Components", 200, 3); // default size: width 200, height 3 lines of text } @Override public Class<RobotProxy> getPaintableClass() { return RobotProxy.class; } @Override protected String getText(RobotProxy robotProxy) { sb.setLength(0); sb.append("Radar: ").append(robotProxy.getActiveRadarName()).append("\n"); sb.append("Drive: ").append(robotProxy.getActiveDriveName()).append("\n"); sb.append("Gun: ").append(robotProxy.getActiveGunName()); return sb.toString(); } }
The above example makes use of the framework's TextPainter class. The framework providers the following painting interface and classes:
- Painter -- root interface for painters within the framework
- AbstractPainter -- abstract painter class that adds support for togging painters on and off through the menu
- WindowPainter -- abstract painter class that extends the AbstractPainter class and provides a window to paint within.
- TextPainter -- abstract painter class that extends the WindowPainter class and is designed for text-only displays.
The Painter Interface
Implementing the Painter interface is the minimum required to be painted by the framework. While you can create a class that implements Painter, it is better to extend one of the framework abstract classes. Otherwise, the Painter will still appear in the menu but you will not be able to toggle the painter on and off.
The AbstractPainter Class
The AbstractPainter class adds support for toggling the painter on and off. Anything that extends AbstractPainter or one of it's subclasses will have a toggle button for it in the menu.
Extend this class for painters that wish to paint over the entire battle field, such as painters that paint bullet waves.
The WindowPainter Class
The WindowPainter class provides a interactive window that the painter can paint within. When using this class, position (0, 0) is the lower left hand corner of the window. Windows can be moved around the screen by dragging them by their title bars, and resized by dragging the resize handle at the bottom right of the window. Not all Graphics2D painting methods are supported when using a WindowPainter; only draw(Shape) and the various drawString(...) methods are supported.
The TextPainter Class
The TextPainter class is a subclass of WindowPainter that makes it a little easier to draw text to the screen. When using a TextPainter, you need only provide the lines of text to print, without having to worry about actually drawing them to the window.
Enabling and Disabling the Framework
The heart of the framework is the PaintManager class. To enable the framework, you need to call one of the enable(...) methods on the PaintManager instance. To disable the framework, comment out or remove the line that calls enable(...).
The arguments to the enable(...) methods activate painting and provide the PaintManager with two things it needs to do its job: the battle field height and the list of Painters.
Example:
// in a one-time setup method somewhere in the robot class PaintManager.getInstance().enable(getBattleFieldHeight(), painters);
Given that Robocode does not provide built-in listeners, you also need to ensure the following methods get called on the PaintManager instance (if you do this manually, you will need to comment out these lines as well when disabling the framework, and uncomment them when enabling the framework):
- public void onPaint(Graphics2D g)
- public void mouseClicked(MouseEvent e)
- public void mousePressed(MouseEvent e)
- public void mouseReleased(MouseEvent e)
- public void mouseDragged(MouseEvent e)
- public void mouseMoved(MouseEvent e)
To make this potentially easier, PaintManager implements MouseListener and MouseMotionListener.
Extending PaintManager to interface with other frameworks
To avoid having to manually call the onPaint, MouseListener, and MouseMotionListener methods, you can optionally extend PaintManager and provide the appropriate code to interface with your own robot.
PaintManager is a Singleton, but you can extend it so long as you implement the static getInstance() method in a manner that initializes the instance in PaintManager, and ensure that you call the getInstance() method on your extended PaintManager class instead of PaintManager the first time you call getInstance() (which is usually when you call enable(...)). As a Singleton, you should also make the constructor private.
As an example, below is the XanderPaintManager class used by the Xander Robot Framework, that extends PaintManager and nicely integrates the Xander Painting Framework into the Xander Robot Framework:
/** * PaintManager to use with the Xander robot framework. If using the Xander * robot framework, make sure you call the getInstance() methods on this class, * and not on the regular PaintManager class. * * @author Scott Arnold */ public class XanderPaintManager extends PaintManager implements PaintListener { private XanderPaintManager() { // RobotEvents is a Xander Robot Framework class that manages events. // It contains all the event listeners needed to ensure all the proper // PaintManager methods get called. RobotEvents robotEvents = Resources.getRobotEvents(); robotEvents.addPainter(this); robotEvents.addMouseListener(this); robotEvents.addMouseMotionListener(this); } // Provide a getInstance() method that initializes the instance variable of the superclass public static synchronized PaintManager getInstance() { if (instance == null) { instance = new XanderPaintManager(); } return instance; } /** * Enable the PaintManager with only the painters that are * part of the Xander robot framework. * * @param battleFieldHeight */ public void enable(double battleFieldHeight) { enable(battleFieldHeight, (List<Painter<? extends Paintable>>)null); } @Override public void enable(double battleFieldHeight, List<Painter<? extends Paintable>> painters) { List<Painter<? extends Paintable>> fullPainterList = new ArrayList<Painter<? extends Paintable>>(); // create all the framework painters first fullPainterList.addAll(XanderPainters.getPainters()); // then add in all the custom painters if (painters != null) { fullPainterList.addAll(painters); } // then pass all painters to the superclass super.enable(battleFieldHeight, fullPainterList); }
This example from the Xander Robot Framework also overrides the primary enable(...) method, and adds another custom enable(...) method. This is not something that needs to be done; the Xander Robot Framework does this to ensure it's own framework painters get added to the PaintManager in addition to any custom painters the robot adds.
Saving and Restoring Painter Settings
Optionally, you can choose to save the state of the painting framework between battles. This allows all of your previously enabled painters, colors, windows sizes, and window locations to be as you left them the next time you use the painting framework.
To use this functionality, make sure the following methods gets called:
- public void restoreState(AdvancedRobot robot) -- call this method after enabling the framework.
- public void saveState(AdvancedRobot robot) -- call this method at the end of the battle.
Note: Don't forget to also comment out the call to these methods as well (or have your custom PaintManager subclass handle it, if you have one) when disabling the framework to reduce the packaged JAR size.
Updating your robot version does not clear out the saved painting framework information. If your robot has changed significantly and you want to clear out previously saved window information, you can either not call restoreState(...), or at any time before calling saveState(...), you can call clearState().
Window information is keyed by the Painter names, so if you change a painter name, the state will not be restored.
Orphan Painters and Paintables
An orphan is any Painter that does not have a Paintable to paint, or any Paintable that does not have a Painter to paint it.
When an orphan Paintable exists, the Xander Painting Framework simply ignores it as if it didn't exist.
When an orphan Painter exists, the Xander Painting Framework assumes that you were expecting it to paint something. Despite having nothing to paint, it will still appear in the paint menu on the screen, but it will appear in the disabled menu color and will not have a toggle button.
Errors
If you have a Paintable that specifies a Painter name, and the corresponding Painter of that name specifies a Paintable class that is not compatible with the actual Paintable class, an IllegalArgumentException will be thrown.
Odds and Ends
Part of the difficulty in developing this framework is integrating it into the limited painting environment that Robocode provides. Some Java painting capabilities are not allowed by Robocode, and some just don't work. On top of that, there are no built-in event listeners for Robocode events nor mouse events. Below is a list of these issues that had to be overcome, how they were overcome, and tips to handling the remaining oddities.
Using Transforms in Robocode
Creating a window that can be moved around the battle field was easier said than done. In order to support movable windows, there needs to be a way to shift the coordinate system. Normally, this could be done by manipulating the Transform associated with the Graphics2D object. However, in the unique Robocode evironment, this is not possible.
Early on, one idea for handling this situation was to create a separate image, create a separate Graphics2D just for that image, and then paint the image onto the main Robocode Graphics2D object. However, Robocode security does not allow you to call the drawImage methods, so this idea was out.
The final solution was to build a proxy class to the Robocode Graphics2D object. The proxy class has some of the same paint methods defined in it as Graphics2D, but before delegating actual painting to the Graphics2D object, the Shape or text position is translated to provide the proper on screen location. Text is as simple as changing the (x,y) coordinate of where the text will be drawn. For Shapes, a Transform is applied to the Shape itself, rather than to the Graphics2D object.
Clipping in Robocode
Another problem with the window system was how to clip the painting such that a window painter cannot paint outside of the window bounds. Much like Transforms, using the standard Graphics2D clipping functions is not an option, as they either don't work or cause exceptions in the Robocode environment.
The solution for clipping Shapes was to use a set of classes specifically designed for clipping Shapes into rectangles. It does this be creating a new GeneralPath for the Shape where all points are forced to remain within the clipping Rectangle. This Clipping to a Rectangle solution is credited to Jeremy Wood, and is covered by a modified BSD license, Copyright (c) 2011, Jeremy Wood. A copy of the BSD license conditions is included in the Javadoc for the Clipper class in Clipper.java.
For clipping text, the FontMetrics instance of the Graphics2D object is used to determine how much of the text will fit within the window bounds. Text that is too long is truncated and an ellipsis is added to the end. Text completely off the screen or text that begins off screen is not printed.
drawString methods utilizing the AttributedCharacterIterator are not clipped.
No Built-In Event Handlers
Another problem with developing this framework was in handling the events, as there are no built-in listeners for a Robocode robot. As such, the user of this framework must ensure that the proper event methods get called. Much of this is detailed in the sections on enabling and disabling the framework (see the appropriate section above). However, a few other minor related details that can help make this framework work more smoothly include:
- Between rounds, mouse events can get lost. This can cause strange behavior if you are interacting with the framework between rounds; for example, if a round ends while dragging a window, upon start of the next round the window can continue to drag even if you are no longer holding the mouse button down. To avoid this problem, if you have extended the PaintManager class, simply set the PaintManager variables dragTarget and resizeTarget to null between rounds (they are declared protected to allow for this). Regardless of whether or not you extended PaintManager, you can also just call mouseReleased(null) on the PaintManager instance between rounds, as this accomplishes the same thing (however, this is not guaranteed to continue to work properly with future updates to the famework).
ToDo's
The following features remain to be implemented:
- Add close button on the window title bars.
- Add ability to change more of the colors and styles used in the framework.
- Improve default color palette.
- Change toggle button enabled images from squares to check marks to make their behavior more obvious.
Obtaining a Copy
Version 1.0 of the Xander Painting Framework source code is available as a ZIP file. In addition to the main painting framework classes in the xander.paint package, it also includes the Xander file utility class in the xander.util package, and the Clipper classes in the com.bric package, both of which are required.