Skinning widgets

Introduction

As of version 4.1.0 released on 11th of November 2013, WireframeSketcher provides an extension mechanism that lets you customize and adapt built-in widgets to better match targeted user interface. This mechanism is based on scripted SVG images with some custom extensions.

Skinning mechanism is meant to be used by stencil developers. Stencil developers can create libraries with assets that match the targeted UI and then distribute them to regular users to be used for wireframing.

Skins

Skin files

Skins are provided as standard SVG files with “.svg” extension. These files can be edited using any vector graphical tool that support SVG format. Inkscape is our preferred tool for that. It’s open-source and is available for free on all platforms.

Skin files must be placed in your project along with “.screen” files. Usually skin files are part of a stencil in which case they will be placed in “assets” folder and named using underscore (“_”) prefix to make them private and hide from the palette view.

The name of the skin file is significant and is used to name the skinned widget. For example “_iOS_toggle.svg” file name will be translated to “iOS Toggle” name that will be assigned to skinned widget. By convention underscore (“_”) and minus (“-”) characters are used as word separators. Words are automatically capitalized unless the second character is already a capital letter like in “iOS”.

Assigning skins

XML format specification describes a “skin” attribute which is a relative reference to an SVG file. This attribute is supported by most widgets, except of the most complex ones like List, Popup, Table and Tree. This attribute can be specified by opening the “.screen” file using a text editor and editing the XML directly. Here’s an example that skins a Progress Bar widget:

<widgets xsi:type="model:ProgressBar" ... skin="_rounded-progress-bar.svg"/>

This operation is reserved to stencil developers and must be repeated for each skinned widget.

Scripting

Skinned widgets inherit all the properties of the built-in widget that is being skinned. For example if there is “background” property then skinned widget must take it in account and use this color in the SVG image. The way it’s done is by using scripting to interpret property values and manipulate SVGs document object model (DOM) to construct the desired result.

Scripting is done by embedding JavaScript inside SVG file using <script> tag. Then handler functions are hooked into SVGs “onload” and “onresize” events. “onload” event is dispatched once when the SVG is loaded. “onresize” is dispatched when the widget is resized but also on any property change. The new size of the widget is set first as “width” and “height” attributes on “svg” element. “onresize” handler is the one responsible for interpreting the new size and property values and manipulating SVG DOM to match them.

Here’s a typical SVG skin file that modifies built-in Progress Bar widget to use rounded look:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" height="20" width="200"
	version="1.1" ws-fill="#ff07ff"
	onload="onload(evt)" onresize="onresize(evt)">
 <script><![CDATA[
 var svg, doc;
 var background, bar, border;
 
 function onload(evt) {
 	svg = evt.target;
 	doc = svg.ownerDocument;
 	background = doc.getElementById("background");
 	bar = doc.getElementById("bar");
 	border = doc.getElementById("border");
 }
 
 function onresize(evt) {
 	var width = parseInt(svg.getAttribute("width"));
 	var height = parseInt(svg.getAttribute("height"));

	var barWidth = (width - 2) * $model.value / 100;
	
	background.setAttribute("width", width - 2); 
 	bar.setAttribute("width", barWidth);
	border.setAttribute("width", width - 2); 

	background.setAttribute("height", height - 2); 
 	bar.setAttribute("height", height - 2);
	border.setAttribute("height", height - 2); 

	background.setAttribute("ry", height / 2); 
	bar.setAttribute("ry", height / 2); 
	border.setAttribute("ry", height / 2);
	
	bar.setAttribute("display",
		barWidth < height && $model.value != 100 ? "none" : "inline"); 
 }
 ]]></script> 
 <rect id="background" ry="9" height="18" width="198" y="1" x="1" fill="#FFF"/>
 <rect id="bar" ry="9" height="18" width="99" y="1" x="1" fill="#ff07ff"/>
 <rect id="border" stroke-dasharray="none" ry="9" height="18" width="198"
 	stroke="#000" stroke-linecap="round" stroke-miterlimit="4" y="1" x="1"
 	stroke-width="2" fill="none"/>
</svg>

Note that it’s also possible to place JavaScript code into an external file and load it using script’s “src” attribute. External script files are resolved relative to the SVG file’s path. In this case script tag looks like this:

<script src="_skin.js"/>

JavaScript code has access to several objects:

Embedding text and icons

When a widget has text items and/or icon these can be embedded in SVG document by using “foreignObject” element. “foreignObject” element specifies the position and size of the item by using “x”, “y”, “width” and “height” attributes. Which item is being embedded is determined by the “id” attribute. Each item is referenced using its index using “item0”, “item1”, “item2” etc as element’s id. Icons are embedded by using “icon” as “foreignObject” element’s id.

It’s also possible to embed free text by using an id that starts with “text” and setting the “text” attribute of foreignObject element to the text to display. The “text” attribute supports wiki style syntax.

“foreignObject” element can also determine the color of the text or icon by using “stroke” attribute. It’s also possible to determine text’s or icon’s color automatically by specifying only the background color using “fill” attribute. In this case the color will be either black or white depending if the background is light or dark.

“foreignObject” elements sometimes can be defined directly in SVG document. However frequently items are dynamic so these elements need to be created dynamically by the script. Also editing tools rarely support “foreignObject” elements directly so scripting is usually how these elements should be created. Here’s an example:

var text = doc.createElement("foreignObject");
text.setAttribute("id", "item" + i);
text.setAttribute("x", ix);
text.setAttribute("y", 0)
text.setAttribute("width", $items[i].width);
text.setAttribute("height", height);
text.setAttribute("fill", "black");
container.appendChild(text);

Note that when coding the “onresize” handler you need to keep in mind that on each call it will find the SVGs DOM in the same state that it was left in by last call. So when “onresize” handler creates and adds elements it needs to make sure to remove elements created by last call. It’s best done by defining and using a clear function like this one:

function clearNode(node) {
	while (node.firstChild) {
		node.removeChild(node.firstChild);
	}
}

Preferred size

All widgets have a preferred size that is set by default. Some widgets like Progress Bar have a pre-set default size that is always the same. For skins this default size is determined from initial “width” and “height” attribute values on “svg” element.

In other cases a more intelligent calculation is required for determining the preferred size. Skins can provide such a calculation by defining “getPreferredSize” function. This function needs to return an object with “width” and “height” properties.

Here’s an example of preferred size calculation for Tabs widgets:

function getPreferredSize() {
 	var width = 0, height = 0;
 	
 	for(var i = 0, n = items.length; i < n; i++) {
 		width += items[i].width;
 		height = Math.max(height, items[i].height);
 	}
 	
 	width += (padding * 2) * items.length;
 	height += (padding * 2);
 	
 	return { width: width, height: height };
 }

Editing skins

You can use WireframeSketcher itself to edit “.svg” files. Select the “.svg” file in Project Explorer view, then right click on it and select Open With > Text Editor. On every save any modification you made will be automatically propagated to screen editors that use the edited skin and you’ll be able to instantly preview your changes.

Debugging scripts

Sometimes it will be necessary to debug your script or track scripting errors. For this you can use the log file that is generated by WireframeSketcher and that is located in <workspace>/.metadata/.log. All errors will be logged there. You can also log your own messages using console.log function. Logging DOM elements will display them formatted as XML in the log file so that you can debug those too.

Help

Note that skinning functionality is still under active development. Please contact us with any issue your encounter while using or developing skins. We’ll help you out and update the documentation if necessary.