Easy Cursor Management
This came up because we decided to refactor the cursor management in the project that I am on at work. It works, but the project has changed a lot since we first started writing it. We had to make changes to the way it worked, as well as accomodate for unexpected changes to the design. Like I said, it works, but it wasn’t as flexible as we would have liked. So I came up with this solution, and think it’s cool enough to share.
Lets take a look at the cursor manager stuff along with an example.
The example is going to be the most simple drawing app you have ever seen. It has two tools a rectangle drawing tool, a line drawing tool, and a canvas to draw onto. When you select a drawing tool and then mouse over the canvas you will see the mouse cursor change to a cross hair, indicating that you are in drawing mode.
Below is an example of the cursor demo in action.
[kml_flashembed movie=”http://specialrelativity.org/blogAssets/CursorDemo.swf” height=”306″ width=”426″ /]
The focus will only be on the code that is related to the cursor stuff we are talking about. We will be relying on event bubbling and a technique I talk about in this post.
How the cursor management works. We store our current cursor in a cursor manager. The current cursor is changed when the cursor manager receives a cursor event through its handleCursorEvent method. The manager can be disabled/enabled, and also contains the display object container of the current cursor. The cursor container is what is added to the screen so you can see the cursor.
You can see the CursorManager here: CursorManager.as
We needed to come up with a convenient way of handling the cursors themselves. A large portion of the application is likely to not use a custom cursor at all. For this reason I spent some time coming up with a way to treat not using a cursor, and using a cursor in the same way. The solution is the Cursor class. The Cursor class prevents us from having to deal with using no custom cursor differently then we do using a custom cursor.
The Cursor class serves as the base of all cursor objects, but also has a special purpose. It represents the special case default cursor (or more appropriately, no custom cursor). We expect the following things out of custom cursor objects.
- They can be enabled/disabled.
- We can tell if it is enabled or not through the isEnabled property.
- The cursor is a display object.
Now, our Cursor class is the base class of our cursors, but it is not abstract. It is simply an empty display object. When enabled it shows the mouse using Mouse.show(), when disabled it hides the mouse using Mouse.hide(). We also implement a basic template pattern here.
When a cursor is added to the stages display list we call the cursor’s enable() method. When we remove the cursor from the stages display list we call the cursor’s finalize() method. The finalize method will be used to destroy all of the event listeners used with the cursor.
This design implies that the cursor is responsible for keeping itself up to date. For instance most custom cursors replace the default cursor, meaning the cursor has to know where the default cursor would be so that it can place itself there. I find this to be the most common case, so common in fact I believe it was worth writing its own base class, which we’ll call GraphicCursor.
GraphicCursor handles the details of listening for the mouse move, and moving itself to the position of the mouse. Since it is a graphic cursor we add a step to the initialization process to draw the cursor. The draw method is abstract, so you must extend GraphicCursor for your own cursors and override its draw method.
You can see both here: Cursor.as and GraphicCursor.as
How changing the cursor works. By their very nature cursors are tied closely to the user interface of whatever it is you are making. I chose to allow the cursor to be changed through events, this is so that elements on the screen can trigger new cursors. This also has the benefit of allowing elements to be safely used in applications that don’t use custom cursors, since the implementation is not bound to the element.
The event of choice is the CursorEvent. There is 3 ways in which we allow the cursor to be changed using this event. The dispatcher of the event can provide the cursor to the cursor manager in a CursorEvent.CHANGE event. It can also pass along information which the receiver of the event can use to decide which Cursor to change to. A specific case which will be used a lot, and was added for convenience, is the CursorEvent.DEFAULT event. The default event changes the cursor back to Cursor, which again means no custom cursor.
You can see the event here: CursorEvent.as
It is important that elements in the screen’s display list broadcast this event, since it relies on event bubbling to make it to the cursor manager. That brings up another topic though. How do we get the event to the cursor manager?
Like I said before the event relies on bubbling. How we organize our application can make a big difference as to how easy this system is to use. Basically you should have one class which controls the flow of your application’s main components. Two of these components should be the screen, and the cursor manager. The application should listen to the screen for cursor events, and pass them on to the cursor manager. It’d be very common to create a choke point that you can filter all of the cursor events through in the application, before sending it to cursor manager.
And that is how the cursor management works. Let’s briefly go over our drawing application demo, I’ll explain basically what is going on, and provide you with all of the source code.
DrawingApp
Basically we create an instance of our drawing app, add it’s screen to the stages display list, and then run the application. Our drawing app contains the screen, the cursor manager, the canvas, and the tools panel. When the application is running it is listening for clicks on the canvas, which will activate whatever drawing tool is currently selected.
The drawing app listens to the screen for CursorEvents. When it receives an event it checks to see if the event contains the cursor to change to. If it doesn’t it will try to decide on which cursor to use based on the events cursorInfo property.
When you roll onto the canvas it will broadcast a CursorEvent, and will populate the events cursorInfo property with the string “toolcursor”. Our application will receive the event, see that no cursor exists, check the cursor info property and will set the events cursor to a new DrawingToolCursor, then it passes the event on to the cursor manager.
When you roll off of the canvas the cursor is changed back to the default cursor. You can see how the Cursor object becomes convenient, because it allows us to work with the other built in techniques for changing the cursor, as is the case with the Tools panel. Each tool has buttonMode set to true when it is not selected, so that if you roll over it the hand cursor will appear. Our cursor management system does not interfere with this at all.
You can see all of the code at the following locations:
March 10th, 2008 at 10:07 am
Nicely done article. Your sample was really easy to follow and well documented.
I have a slightly different problem. We have an application that we are building that needs to change the cursor based on a location within a timeline - and not just in response to a MouseEvent. So at certain times we need a mouse pointer (default cursor) and at other times we need a hand cursor.
We have been able to change the cursor using the “buttonMode” and “useHandCursor” methods. The only problem that we have encountered is that if we just let the app run (meaning the timeline is advancing) without interacting at all with the user interface (meaning NO mouse movement or clicks), we can see the cursor change from default (mouse pointer) to hand cursor. However as the timeline advances, the cursor never changes back UNLESS you move or click the mouse.
We know that the event has fired to change the cursor back. However its visual appearance does not change without the mouse event.
In other languages there are usually methods to “force” a repaint - something that would cause the cursor to update.
I have tried a few things like “updateAfterEvent” on the triggering event - but I have not been successful in getting the cursor to change.
Do you have any suggestions? I was thinking about generating a mouse event on our own - maybe?
Thanks
March 10th, 2008 at 10:28 am
Right after I posted my last message - I found something that helped me - so now my problems appears to be fixed. I simply called Mouse.hide() followed immediately Mouse.show() and now the cursor updates it appearance even without a mouse event. It was your sample though that led me in the right direction. Thank you.