‘Sprite’ Buttons in ScriptUI [UPDATE]
July 05, 2013 | Tips | en
A sprite usually refers to a primary image which is integrated in a larger scene. The term was popularized by Dave Shea in his 2004 article “CSS Sprites: Image Slicing’s Kiss of Death” which describes how to render the multiple states of a button or icon from a single composite image. I discovered that we can derive this technique to create attractive buttons in ScriptUI 4.0 and later…
Originally posted on April 6, 2011. Updated to support InDesign CC. See details below.
The starting point is very simple: instead of using the native ScriptUI Button
wrapper, we want to display a pure Image
object that will receive mouse events and behave like a button.
Note. — The Image
widget is poorly documented and should not be confused with the ScriptUIImage
structure that encapsulates a few properties of an actual image. For its part, the Image
widget is a simple container and it virtually operates as a Group
. Although it may remain empty it is designed to contain a ScriptUIImage
that is accessed to by the image
(or icon
) property.
Since ScriptUI 4.0, every component —including passive widgets such as Group
and Image
— is allowed to dispatch mouseover
, mousedown
, mouseout
, and mousemove
events. Hence we can write event listeners to make this kind of widget more reactive. Of course it is possible —though not so obvious!— to dynamically change the underlying bitmap of an Image
object. But why not trying to break the back of the beast with a single image?
Three Sprites under the hood
To mimic a reactive button, what we basically need is a three-state component, as shown below:
An important prerequisite is that the different states of this triptych have the same dimensions. Now let's see the situation from a ‘sprite’ perspective:
The above screen shows the sliced PNG image (1), and the state of the GUI (2) when the mouse rolls over the widget. All we have to do is to appropriately shift the image along the vertical axis of that container. The container acts like a mask: it displays the relevant part of the bitmap within its own area.
Technically, the whole trick is based on the ScriptUIGraphics.drawImage(…)
method, defined as follows:
drawImage
(image, x, y, width, height)
where:
• image refers to a ScriptUIImage
object;
• x (Number
) is the (positive or negative) left coordinate of the region, relative to the origin of the element (0 is default);
• y (Number
) is the (positive or negative) top coordinate of the region, relative to the origin of the element (0 is default);
• width and height (Number
) are the dimensions of the image. If provided, the image is stretched or shrunk to fit. If omitted, uses the original image dimensions.
As you probably guessed, we are going to play with the y argument…
Further in the code
There are two critical issues to be aware of:
• By default, an Image
widget —i.e. the container— will get the size of the underlying bitmap at construction time, provided that we set the content at construction time and that we do not override the default mechanism:
myImage = myWindow.add('image', undefined, myBitmap)
.
Note that myBitmap might be: a File
(PNG, JPEG), a ‘resource’ code (see Peter Kahrel's ScriptUI for Dummies for advanced details on this topic), a ScriptUIImage
, or even a String
that directly contains the bytes of the bitmap —undocumented but really cool feature!
Then, in order to use the sprite-based strategy, we have to explicitly set the size
of the container by dividing by three the height of the image. As I wanted to keep my code as generic as possible, I preferred not to hard-code the size of any component. Therefore I let the default mechanism detects the actual size of the supplied bitmap, then I refine the size of the container:
// . . . // Number of vertical slices // --- var V_SPRITES = 3; // Create the UI // --- var w = new Window('dialog',"ScriptUI Sprites"), myImage = w.add('image', undefined, myPNG), iSize = myImage.image.size, spriteHeight = iSize[1] / V_SPRITES; myImage.size = [iSize[0], spriteHeight]; // . . .
• Another important point is that we cannot invoke myImage.graphics.drawImage(...)
without precaution. As far as I know, such operation is only available during the draw event, hence in the scope of an onDraw
routine. That's why the mouse event handler artificially forces a redraw via the custom Image.prototype.refresh()
method we have implemented in v. 1.02 of the sample script. The previous version was invoking myImage.parent.layout.layout(true)
, an old way to trigger onDraw
, but that trick does not work in CC anymore and, to be honest, this was not an elegant solution. Here is the new routine:
// Force an Image widget to repaint itself (= onDraw trigger) // CS4-CS6 -> just reassigning this.size // CC -> we need to temporarily *change* the size // Note: using layout.layout(1) would not work anymore in CC // --- const CC_FLAG = +(9 <= parseFloat(app.version)); Image.prototype.refresh = CC_FLAG ? function() { var wh = this.size; this.size = [1+wh[0],1+wh[1]]; this.size = [wh[0],wh[1]]; wh = null; }: function() { this.size = [this.size[0],this.size[1]]; };
InDesign CC Compatibility Note #1. — The key idea, as you can see, is to reset the size
property of the widget. This seems stupid at first glance, as we just reassign the same width and height to this.size
. But ScriptUI internally detects this assignment and then triggers this.onDraw
, which we couldn't do directly in a secure way. However, this trick does not work as easily in CC. Indeed, ScriptUI CC is somewhat smarter than its previous versions in that it only calls onDraw
if the size is actually modified. So we have implemented a compatibility patch in which we temporarily increase the size by 1 pixel then revert to the original value.
InDesign CC Compatibility Note #2. — In the previous versions of ScriptUI we observed that when the image is shifted within its container by a non-zero offset we needed to compensate the move for 1 pixel. This bug is now fixed in ScriptUI CC. For that reason I introduced a constant FIX_OFFSET
which I set to 0 in CC only.
Finally, here is the main routine of the script:
// . . . myImage.onDraw = function() { var dy = this.properties.state*spriteHeight + FIX_OFFSET; this.graphics.drawImage(this.image,0,-dy); }; var mouseEventHandler = function(ev) { // Update the 'state' of myImage (internal flag) // --- this.properties.state = ('mouseover'==ev.type)+ 2*('mousedown'==ev.type); // Force onDraw (custom prototyped method) // --- this.refresh(); }; // Register the mouse event handler // --- myImage.addEventListener('mouseover', mouseEventHandler); myImage.addEventListener('mousedown', mouseEventHandler); myImage.addEventListener('mouseup', mouseEventHandler); myImage.addEventListener('mouseout', mouseEventHandler); // . . .
I hope you'll enjoy customizing ScriptUI buttons!
Comments
Hi Marc,
Only the other day did I discover that iconbutton can take a ScriptUI.newImage argument which itself can take up to four graphic files:
w.add ("iconbutton", undefined, ScriptUI.newImage (icon_a, icon_b, icon_c, icon_d));
These four files respond automatically to default, [don't know], click, mouseover.
Peter
Hi Peter,
Thanks for adding this useful clarification. I intended to mention the IconButton case in a note... but was afraid to bog down my post ;-)
Indeed, IconButton supports the 'full' ScriptUIImage object:
ScriptUI.newImage(normal, disabled, pressed, rollover)
This prototype suggests that, in fact, any ScriptUIImage may also encapsulate *a set* of bitmaps. And this perfectly works with IconButtons.
But... I never managed to inject a full set of bitmaps in an Image container, even by explicitly sending a 4-state ScriptUIImage in the add('image',...) method —perhaps I missed something, did I?
For the time being my thought is that the Image object does not support this feature although it is syntactically supposed to.
Then, I agree we can get pretty similar results with a simple IconButton, except that IB comes with its own OS-based skin and behavior. The 'style' creation property sets the button vs. toolbutton option, while the 'toggle' creation property sets the button-pressed appearance.
What I wanted to focus on in my article is the possibility to inhibit *any* effect induced by the OS, and to fully customize a fake button from scratch.
I hope it also opens up cool perspectives on how to play with bitmaps position/size within an Image area.
@+
Marc
Your method is clearly more versatile and OS-aesthetics-independent than what the ScriptUI iconbuttons have to offer.
But I'm not sure that it's correct to say that the image control is syntactically supposed to support four-state images. I thought that that was a property only of iconbuttons.
Thanks for the educational example!
Peter
> I'm not sure that it's correct to say that the image control
> is syntactically supposed to support four-state images.
> I thought that that was a property only of iconbuttons.
The facts show that you are right. However, since the Image.icon property is a ScriptUIImage, and since the ‘constructor’ of a ScriptUIImage object is ScriptUI.newImage(normal, disabled, pressed, rollover), my reasoning was that an Image object could support a four-state element... I was wrong but the prototype doesn't seem to preclude this possibility, does it?
@+
Marc
True, ScriptUIImage's prototype doesn't preclude the possibility you sketch, it's just that the image control doesn't like it. Maybe time for a feature request.
Peter