Chapter 17. Layout and Design

Table of Contents

1. Declarative positioning of views using layouts
2. Horizontal and Vertical Boxes
3. Variations on Layout
3.1. StableBorderLayout
3.2. Constant Layout
4. Design considerations in LZX programs
4.1. The view hierarchy
5. Scripting and Layouts
5.1. Addressing Layouts
5.2. Layouts Array
5.3. Locking and Unlocking Layouts
6. Animating layout
7. Writing Custom Layouts
7.1. The update() method

LZX enables the creation of applications that have high design values and fluid, graceful interaction. In this chapter we discuss some basic tools for layout and design. For more advanced topics and exploration of the aesthetics of OpenLaszlo application design, see the Designer's Guide.

1. Declarative positioning of views using layouts

By default, all views position themselves at x="0" y="0" in their coordinate space. That means that two sibling views will sit on top of each other. Aside from explicitly positioning the views so they don't overlap, the easiest way to separate the siblings is to use a simplelayout:

Instead of specifying each view's coordinates (either through a constraint or absolutely), you can declare a layout that decides how sibling views position themselves relative to one another. The simplest layout is simplelayout, which positions sibling elements beneath (or to the right of) each other:

Example 17.1. Simple Layout

<canvas height="80" width="100%">
  <view bgcolor="yellow" width="100" height="100">
    <simplelayout axis="y" spacing="5"/>
    <view bgcolor="red" width="20" height="20"/>
    <view bgcolor="blue" width="20" height="20"/>
    <view bgcolor="green" width="20" height="20"/>
  </view>
</canvas>

Here's an example that shows how to use layouts to resize views in response to user actions:

Example 17.2. Adding a view to a layout

<canvas height="140" width="100%">
  <class name="coloredRect" width="20" height="80" onclick="this.setAttribute('width', this.width+2)"/>
  <view x="10" y="10" name="container">
    <simplelayout axis="x" spacing="10"/>
    <coloredRect bgcolor="red"/>
    <coloredRect bgcolor="aqua"/>
    <coloredRect bgcolor="yellow"/>
    <coloredRect bgcolor="green"/>
    <coloredRect bgcolor="blue"/>
  </view>
  
  <button x="10" y="110">Create A New View
    <handler name="onclick">
      var newView = new lz.coloredRect(container, { bgcolor : 0x000000 });
    </handler>
  </button>
</canvas>

Like all layouts, simpleLayout affects how sibling views relate to one another. This remains true even if views are dynamically instantiated. You can set both the axis of a simplelayout as well as the distance between the elements. Also note layouts update as the dimensions of the affected views change.

If you need a specific group of views to be affected by a simplelayout, but want them to be isolated from the rest of the application, encase them in a parent view. You can then specify coordinates on the parent view:

Example 17.3. Isolating views

<canvas height="120" width="100%">
  <class name="coloredRect" width="20" height="80"/>
  <view width="50" height="50" bgcolor="yellow"/>
  <!-- Group of horizontally stacked views, isolated from app -->
  <view name="container" x="175" y="20">
    <simplelayout axis="x" spacing="10"/>
    <coloredRect bgcolor="red"/> 
    <coloredRect bgcolor="blue"/> 
    <coloredRect bgcolor="teal"/> 
    <coloredRect bgcolor="black"/> 
  </view>
</canvas>

There may be times when you would like to select between more than one layout in which to display your view. Options are a good way to do this You then override addSubview in your layout as shown in the code below. To get a better idea of what's going on, run the program with the debugger on.

Example 17.4. Choosing between layouts

<canvas height="400" width="100%">
  <debug fontsize="12"/>
  <class name="box" width="50" height="30"/>
  <view name="myview">
    <simplelayout axis="x" spacing="4">
      <method name="addSubview" args="sv">
            if ( sv.getOption( 'goesInLayout1' ) ){
                super.addSubview( sv );
                Debug.debug("adding %w, %w to layout1", sv, sv.name);
            }          
        </method>
    </simplelayout>
    <simplelayout axis="y" spacing="4">
        <method name="addSubview" args="sv">
            if ( sv.getOption( 'goesInLayout2' ) ){
                super.addSubview( sv );
                Debug.debug("adding %w %w to layout2", sv, sv.name);
            }
        </method>
    </simplelayout>
    <box bgcolor="red" options="goesInLayout1" name="red1"/>
    <box bgcolor="red" options="goesInLayout1" name="red2"/>
    <box bgcolor="red" options="goesInLayout1" name="red3"/>
    <box bgcolor="blue" options="goesInLayout2" name="blue1"/>
    <box bgcolor="blue" options="goesInLayout2" name="blue2"/>
    <box bgcolor="blue" options="goesInLayout2" name="blue3"/>
    <!-- both -->
    <box bgcolor="yellow" options="goesInLayout2;goesInLayout1" name="yellow1"/>
    <box bgcolor="yellow" options="goesInLayout2;goesInLayout1" name="yellow2"/>
    <box bgcolor="yellow" options="goesInLayout2;goesInLayout1" name="yellow3"/>
  </view>
</canvas>

2. Horizontal and Vertical Boxes

In addition to layouts, LZX includes the <hbox> and <vbox> tags that allow you to group views horizontally and vertically. Unlike layouts, which are objects that affect the behavior of their siblings, <hbox> and <vbox> are themselves views that constrain the layout of their children. Each <hbox> or <vbox> contains a layout, and the tag syntax allows you to set the layout attributes directly on the hbox or vbox.

You can also combine <hbox> and <vbox> elements to form tables. The following example illustrates this. (There are more elegant ways to achieve this effect; we include it here to illustrate the principles.)

Example 17.5. hbox and vbox

<canvas height="120" width="100%">
    <hbox spacing="10">
    <vbox spacing="10" name="column1" bgcolor="red">
        <handler name="onclick">
            this.animate('spacing', this.spacing == 10 ? 0 : 10, 1000);
        </handler>
        <text>click</text>
        <text>to</text>
        <text>animate</text>
        <text>spacing</text>
    </vbox>
    <vbox spacing="10" name="column2" bgcolor="blue">
        <handler name="onclick">
            this.animate('spacing', this.spacing == 10 ? 0 : 10, 1000);
        </handler>
        <text>click</text>
        <text>to</text>
        <text>animate</text>
        <text>spacing</text>
    </vbox>
    <vbox spacing="10" name="column3" bgcolor="yellow">
        <handler name="onclick">
            this.animate('spacing', this.spacing == 10 ? 0 : 10, 1000);
        </handler>
        <text>click</text>
        <text>to</text>
        <text>animate</text>
        <text>spacing</text>
    </vbox>
    </hbox>
</canvas>

3. Variations on Layout

As mentioned previously, layouts act upon sibling elements, but they can control more than just spacing. For example, they can be used to cause views to grow and shrink, as shown below.

3.1. StableBorderLayout

A frequent layout problem when designing GUIs is to have an element stretch while its two surrounding siblings maintain a constant size across the stretch axis. LZX solves this problem with the stableborderlayout:

Example 17.6. Stableborder Layout

<canvas height="140" width="100%">
  <class name="coloredRect" height="80"/>
  
  <view x="10" y="10" name="container" width="200">
    <stableborderlayout axis="x"/>
    <coloredRect bgcolor="navy" width="20"/>
    <coloredRect bgcolor="yellow"/>
    <coloredRect bgcolor="navy" width="20"/>
  </view>
  
  <button x="205" y="110">Increase width of container
    <handler name="onclick">
      container.setAttribute("width", container.width + 10);
    </handler>
  </button>
  
  <button x="10" y="110">Decrease width of container
    <handler name="onclick">
      container.setAttribute("width", container.width - 10);
    </handler>
  </button>
</canvas>

As the name suggests, this layout can be used to create a border with a stable dimension (width or height). stableborderlayout is very useful for creating borders, frames and windows. In this example, stableborderlayout is combined with some constraints to create a 5 pixel blue border:

Example 17.7. Creating a border

<canvas height="200" width="100%">
  <!-- Sliders to control width and height -->
  <text x="200" y="22">Set the width:</text>
  <slider id="widthSlider" x="200" y="50" width="100" value="50" minvalue="20" maxvalue="100"/>
  
  <text x="200" y="92">Set the height:</text>
  <slider id="heightSlider" x="200" y="120" width="100" value="50" minvalue="20" maxvalue="100"/>
  
  <!-- Blue border -->
  <view width="${widthSlider.value}" height="${heightSlider.value}" x="20" y="20">
    <stableborderlayout axis="y"/>
    <view bgcolor="blue" height="5" width="${parent.width}"/>
    <view width="${parent.width}">
      <stableborderlayout axis="x"/>
      <view bgcolor="blue" width="5" height="${parent.height}"/>
      <view height="${parent.height}"/>
      <view bgcolor="blue" width="5" height="${parent.height}"/>
    </view>
    <view bgcolor="blue" height="5" width="${parent.width}"/>
  </view>
</canvas>

If you wanted to use the above code to frame a particular view irrespective of where it was in the view hierarchy, a good way would be to use a floating view:

Example 17.8. Using a floating view

<canvas width="100%">
  <!-- A single colored box -->
  <class name="coloredBox" width="50" height="50" bgcolor="0xff0000"/>
  <!-- Yellow border; invisible at start -->
  <view id="floater" width="50" height="50" visible="false">
    <!-- Animate to the coordinates of a given view -->
    <method name="goTo" args="whichBox">
      this.setAttribute("visible", true);
      this.bringToFront();
      var destX = whichBox.getAttributeRelative("x", canvas);
      var destY = whichBox.getAttributeRelative("y", canvas);
      this.animate("x", destX, 1000, false);
      this.animate("y", destY, 1000, false);
    </method>
    <stableborderlayout axis="y"/>
    <view bgcolor="0xFFFF00" height="5" width="${parent.width}"/>
    <view width="${parent.width}">
      <stableborderlayout axis="x"/>
      <view bgcolor="0xFFFF00" width="5" height="${parent.height}"/>
      <view height="${parent.height}"/>
      <view bgcolor="0xFFFF00" width="5" height="${parent.height}"/>
    </view>
    <view bgcolor="0xFFFF00" height="5" width="${parent.width}"/>
  </view>
  <!-- Controls -->
  <view bgcolor="yellow">
    <simplelayout axis="y" spacing="10"/>
    <button onclick="floater.goTo(blueBox)">Go to Blue Box</button>
    <button onclick="floater.goTo(redBox)">Go to Red Box</button>
    <button onclick="floater.goTo(greenBox)">Go to Green Box</button>
  </view>
  <!-- View hierarchy -->
  <view x="20" y="15">
    <coloredBox id="blueBox" bgcolor="0x0000ff" x="200"/>
    <view x="5" y="40">
      <coloredBox id="greenBox" bgcolor="0x00ff00" y="150" x="13"/>
      <view x="1" y="90">
        <coloredBox id="redBox" bgcolor="0xff0000" y="150"/>
      </view>
    </view>
  </view>
</canvas>

3.2. Constant Layout

The <constantlayout> tag is a way of defining a margin. For example, if all of the children of a container needed to be indented 10 pixels from the left hand edge of the container:

Example 17.9. Constant Layout

<canvas height="140" width="100%">
  <class name="coloredRect" width="60" height="15"/>

  <view x="10" y="10" name="container" bgcolor="silver" width="80" height="120">
    <constantlayout axis="x" value="10"/>
    <simplelayout axis="y" spacing="10"/>
    <coloredRect bgcolor="red" width="40"/>
    <coloredRect bgcolor="aqua" width="50"/>
    <coloredRect bgcolor="yellow"/>
    <coloredRect bgcolor="green" width="20"/>
  </view>
</canvas>

Here <constantlayout> and <simplelayout> are combined so that the <constantlayout> controls the left, whereas the<simplelayout> dictates the vertical spacing between the siblings.

4. Design considerations in LZX programs

This section discusses layouts, which are constructs that allow you to focus on larger design issues by freeing you from the tedium of positioning views. Layouts are implemented with constraints, which are described in Chapter 27, Constraints. For more on designing the "look and feel" of OpenLaszlo applications, please see Chapter 3, OpenLaszlo for Designers.

4.1. The view hierarchy

Views can be nested inside one another, and they can reference other parts of the view hierarchy using dot syntax and/or IDs. The view hierarchy helps you define how the various elements of your application relate to one another. It is helpful to break your application into various elements. Consider a photo-viewing application that has a list of images in a right-hand column, and a main stage area for displaying the images to the left. There is also a trash area that we'll cover later. At a very high level, you might break your application up as follows:

Example 17.10. Outline of photo application

<canvas width="100%">
 <view>
  <view name="stage" width="400" height="300" x="20" bgcolor="0xeaeaea">
    <!-- Area for viewing images -->
    <text>Stage</text>
    <!-- ... -->
  </view>
  
  <view name="listOfImages" width="200" height="300" x="500" bgcolor="0xffff00">
    <!-- Data-replicated list of images -->
    <text>List of images</text>
    <!-- ... -->
  </view>
  
  <view name="trash" width="200" height="75" x="500" y="310" bgcolor="0xff0000">
    <!-- Trash for disposing of images -->
    <text>Trash</text>
    <!-- ... -->
  </view>
 </view>
</canvas>

You can use views with background colors when mocking up applications as shown above, to make them easier to prototype.

If you approach the application in this manner, you've effectively broken it up into manageable pieces already. listOfImages, stage and trash could all be developed independently of one another. listOfImages would contain the text-list of images that are bound to some data with the descriptive text showing. When the user clicks one of the images, it would call a showImage() method on stage with the URL of the image to show as an argument.

The replicated views inside of listOfImages would best be positioned using a layout.

4.1.1. Floating views

The view hierarchy could also be a constraint. Imagine that in the above example, you wanted to be able to drag an item from the list of image names from listOfImages to trash. Frequently in drag-and-drop environments, the original item that was dragged is left in place (although some indication may be given that it is in a transient state, such as being faded out), while a copy of it is dragged. If each of the image titles was draggable, then not only might a dragged item disrupt the arrangement of its siblings, but you wouldn't be able to leave the original intact, in order to obtain the behavior described above.

To get around this problem, you could use a floating view as the draggable view. A floating view is one that floats on top of the rest of your application, and only appears when dragging:

  1. User clicks and holds on a view.

  2. Floating view made visible, and located over clicked view.

  3. Any visual changes applied to clicked view (e.g. color/alpha transform).

  4. As mouse moves around, the floating view follows the mouse.

  5. Onmouseup the floating view disappears, any actions performed, original clicked view restored.

To position the floating view over the clicked view, you can use the getAttributeRelative() method of view to obtain the x and y coordinates of the view relative to a view other than its parent. For example, consider the case below:

Example 17.11. Positioning a "floating view"

<canvas width="100%">
  <!-- An item that will get clicked and dragged -->
  <class name="imageRow" bgcolor="0x93A5FF" width="$once{parent.width}" height="20">
    <attribute name="text" type="text"/>
    <text name="txt" text="${parent.text}" resize="true"/>
    <handler name="onmousedown">
      this.f = new lz.view(canvas, {width: this.width,
                                   height: this.height,
                                   bgcolor: this.bgcolor,
                                   x: this.getAttributeRelative("x", canvas),
                                   y: this.getAttributeRelative("y", canvas),
                                   clickable: true
                                   });
      // Make view follow mouse around
      this.x_offset = this.getMouse("x");
      this.y_offset = this.getMouse("y");
      this.startDraggingFloater();
    </handler>
    
    <method name="startDraggingFloater">
      this.d = new LzDelegate(this, "adjustFloaterPosition", lz.Idle, "onidle");
      this.gm = new LzDelegate(this, "cancelFloater", lz.GlobalMouse, "onmouseup");
    </method>
    
    <method name="adjustFloaterPosition" args="v">
      this.f.setAttribute("x", canvas.getMouse("x")-this.x_offset);
      this.f.setAttribute("y", canvas.getMouse("y")-this.y_offset);
    </method>
    
    <method name="cancelFloater" args="v">
      this.gm.unregisterAll();
      this.d.unregisterAll();
      this.f.destroy();
    </method>
  </class>

 <view>
  <view name="stage" width="400" height="300" x="20" bgcolor="0xeaeaea">
    <!-- Area for viewing images -->
    <text>Stage</text>
    <!-- ... -->
  </view>
  
  <view name="listOfImages" width="200" height="300" x="500" bgcolor="0xffff00">
    <!-- Data-replicated list of images -->
    <simplelayout axis="y" spacing="2"/>
    
    <imageRow>One</imageRow>
    <imageRow>Two</imageRow>
    <imageRow>Three</imageRow>
  </view>
  
  <view name="trash" width="200" height="75" x="500" y="310" bgcolor="0xff0000">
    <!-- Trash for disposing of images -->
    <text>Trash</text>
    <!-- ... -->
  </view>
 </view>
</canvas>

To achieve this, you could create a view from script when the mouse goes down over one of the views you want to drag:

This code is not easy to read, although conceptually it's fairly simple — "create a new view when the mouse button is depressed, and drag it until the mouse button is released". Note also that you have to listen for the global onmouseup event instead of the onmouseup on the dynamically created view. Because it was positioned under the cursor after the mouse was down, the event system does not get notified when the mouse goes up over it.

This approach is fairly laborious — you have to copy all of the attributes of the original view to the new one. If you wanted to have the text field appear in the dragged view, you would have to create a new text field to match it at run-time. As the dragged view becomes more complex, it will take longer to instantiate when the user depresses the mouse button, since view instantiation is fairly processor-expensive.

A better approach would be to have the floating view ready in advance:

Example 17.12. Building a "floating view"

<canvas width="100%">
  <!-- A draggable floating view -->
  <class name="floater" height="20" bgcolor="0x93A5FF" visible="false">
    <dragstate name="dragger"/>
    <text name="txt" width="100"/>
    <method name="showFloater" args="xpos, ypos, itemwidth, itemlabel">
      myfloater.bringToFront();
      this.setAttribute("x", xpos);
      this.setAttribute("y", ypos);
      this.setAttribute("width", itemwidth);
      this.txt.setAttribute("text", itemlabel);
      this.gm = new LzDelegate(this, "cancelFloater", lz.GlobalMouse, "onmouseup");
      this.setAttribute("visible", true);
      this.dragger.apply();
    </method>

    <method name="cancelFloater" args="v">
      this.dragger.remove();
      this.setAttribute("visible", false);
    </method>
  </class>
  
  <!-- An instance of the floater -->
  <floater id="myfloater"/>
  
  <!-- An item that will get clicked and dragged -->
  <class name="imageRow" bgcolor="0x93A5FF" width="$once{parent.width}" height="20">
    <attribute name="text" type="text"/>
    <text name="txt" text="${parent.text}" resize="true"/>
    <handler name="onmousedown">
      myfloater.showFloater(this.getAttributeRelative("x", canvas),
                            this.getAttributeRelative("y", canvas),
                            this.width,
                            this.txt.text);
    </handler>
  </class>

 <view>
  <view name="stage" width="400" height="300" x="20" bgcolor="0xeaeaea">
    <!-- Area for viewing images -->
    <text>Stage</text>
    <!-- ... -->
  </view>
  
  <view name="listOfImages" width="200" height="300" x="500" bgcolor="0xffff00">
    <!-- Data-replicated list of images -->
    <simplelayout axis="y" spacing="2"/>
    <imageRow>One</imageRow>
    <imageRow>Two</imageRow>
    <imageRow>Three</imageRow>
  </view>
  
  <view name="trash" width="200" height="75" x="500" y="310" bgcolor="0xff0000">
    <!-- Trash for disposing of images -->
    <text>Trash</text>
    <!-- ... -->
  </view>
 </view>
</canvas>

This code is much cleaner, and the floater code is now completely independent of the imagerow code. Individual properties of the floater can still be set (in this case they're passed as arguments to the showFloater method).

Taking this a step further, if our list of images were replicated against a dataset an even more elegant approach would be to pass the data node of the replicated imageRow instance to the floater, and have the floater do the right thing with the data.

Example 17.13. Using a dataset to drive layouts

<canvas width="100%">
  <!-- The data that provides the images -->
  <dataset name="ds_items">
    <items>
      <item description="One" src="one.jpg"/>
      <item description="Two" src="two.jpg"/>
      <item description="Three" src="three.jpg"/>
    </items>
  </dataset>
  
  <!-- A draggable floating view -->
  <class name="floater" height="20" bgcolor="0x93A5FF" visible="false" datapath="">
    <dragstate name="dragger"/>
    
    <text name="txt" datapath="@description" resize="true"/>
    <text name="imgurl" datapath="@src" resize="true" align="right"/>
    
    <method name="showFloater" args="xpos, ypos, itemwidth, dataNode">
      myfloater.bringToFront();
      this.setAttribute("x", xpos);
      this.setAttribute("y", ypos);
      this.setAttribute("width", itemwidth);
      this.datapath.setAttribute("p", dataNode);
      this.gm = new LzDelegate(this, "cancelFloater", lz.GlobalMouse, "onmouseup");
      this.setAttribute("visible", true);
      this.dragger.apply();
    </method>
    
    <method name="cancelFloater" args="v">
      this.dragger.remove();
      this.setAttribute("visible", false);
    </method>
  </class>
  
  <!-- An instance of the floater -->
  <floater id="myfloater"/>
  
  <!-- An item that will get clicked and dragged -->
  <class name="imageRow" bgcolor="0x93A5FF" width="$once{parent.width}" height="20">
    <attribute name="text" type="text"/>
    <text name="txt" datapath="@description" resize="true"/>
    <handler name="onmousedown">
      myfloater.showFloater(this.getAttributeRelative("x", canvas),
                            this.getAttributeRelative("y", canvas),
                            this.width,
                            this.datapath.p);
    </handler>
  </class>

 <view>
  <view name="stage" width="400" height="300" x="20" bgcolor="0xeaeaea">
    <!-- Area for viewing images -->
    <text>Stage</text>
    <!-- ... -->
  </view>
  
  <view name="listOfImages" width="200" height="300" x="500" bgcolor="0xffff00" datapath="ds_items:/items">
    <!-- Data-replicated list of images -->
    <simplelayout axis="y" spacing="2"/>
    <imageRow datapath="item"/>
  </view>
  
  <view name="trash" width="200" height="75" x="500" y="310" bgcolor="0xff0000">
    <!-- Trash for disposing of images -->
    <text>Trash</text>
    <!-- ... -->
  </view>
 </view>
</canvas>

The floater must have an empty datapath assigned in advance. Notice how since only a <datapointer> element is passed to the floating view, the floater can access attributes of the data that were not referenced in the imageRow class (for example,the src attribute). This can become particularly useful when you want to "drop" the floater somewhere. The drop target may need to access other data properties (or even delete the XML node in the dataset), and passing a pointer to it saves retracing it.

A floating view is not only for draggable objects. If you wanted to frame a specified view, regardless of where it was in the view hierarchy, you might also find a floating view to be the best option. See the StableBorderLayout section for more.

4.1.2. Resources and ignoring layouts

You should avoid placing views inside of views with a resource. Instead, to have a background image in a container view, have a view that attaches the resource be a child of the parent view. Use options="ignorelayout" to prevent the background image from being affected by any layouts that act upon the children of its container:

Example 17.14. ignorelayout

<view name="container">
  <view resource="someimg.jpg" options="ignorelayout"/>
  <view name="childView"/>
</view>

5. Scripting and Layouts

5.1. Addressing Layouts

Since a layout extends Node, it can be addressed using dot syntax:

<canvas height="140">
  <class name="coloredRect" width="20" height="80"/>

  <view name="container">
    <simplelayout name="slayout" axis="x" spacing="10"/>
    <coloredRect bgcolor="red"/>
    <coloredRect bgcolor="aqua"/>
    <coloredRect bgcolor="yellow"/>
  </view>
</canvas>

In the above example, the layout could be accessed by: container.slayout.

You might need to address a layout in script, in order to change one of its attributes at run-time, to call the layout's update() method, or to lock it. A layout can also have an id, so that it can be accessed globally.

5.2. Layouts Array

All nodes have a layouts array, which contains pointers to all of the layouts that act upon its children:

Example 17.15. Addressing layouts

<canvas height="180" width="100%">
  <class name="coloredSquare" width="20" height="20"/>

  <view name="container" bgcolor="silver" x="10" y="10">
    <simplelayout axis="x" spacing="2"/>
    <simplelayout id="yLayout" axis="y" spacing="2"/>

    <coloredSquare bgcolor="red"/>
    <coloredSquare bgcolor="red"/>
    <coloredSquare bgcolor="red"/>
    <coloredSquare bgcolor="red"/>
    <coloredSquare bgcolor="red"/>
  </view>

  <button y="150" x="10">Increase spacing of BOTH x and y layouts
    <handler name="onclick"><![CDATA[
      
      for (var i=0; i<container.layouts.length; i++) {
          var lyt = container.layouts[i];
          lyt.setAttribute('spacing', lyt.spacing + 2);
      }
      
    ]]></handler>
  </button>

  <button y="150" x="280">Increase spacing of y layout ONLY
    <handler name="onclick">
      yLayout.setAttribute('spacing', yLayout.spacing + 2);
    </handler>
  </button>
</canvas>

5.3. Locking and Unlocking Layouts

Layouts can be locked and unlocked using the lock() and unlock() methods respectively (inherited from the layout class). When you lock a layout, you prevent it from updating. For example, if you click the colored boxes in the following example to enlarge them, the simplelayout will automatically update, and the spacing between the boxes will remain consistent. However, if you lock the layout, then grow the boxes, the simplelayout will not update, until the unlock() method is called on the layout:

Example 17.16. Locking and unlocking layouts

<canvas height="140" width="100%">
  <class name="coloredRect" width="20" height="80" onclick="this.setAttribute('width', this.width+2)"/>

  <view x="10" y="10" name="container">
    <simplelayout name="slayout" axis="x" spacing="10"/>
    <coloredRect bgcolor="red"/>
    <coloredRect bgcolor="aqua"/>
    <coloredRect bgcolor="yellow"/>
    <coloredRect bgcolor="green"/>
    <coloredRect bgcolor="blue"/>
  </view>

  <button x="10" y="110">Lock
    <handler name="onclick">
      container.slayout.lock();
    </handler>
  </button>

  <button x="70" y="110">Unlock
    <handler name="onclick">
      container.slayout.unlock();
    </handler>
  </button>
</canvas>

The lock() method sets the layout's locked attribute to true. The unlock() method calls the layout's update() method, then sets the locked attribute to false.

6. Animating layout

Layouts can be animated; that is, the properties of the layout can be set to vary over time. For an example and an explanation of how this is done, see Chapter 24, Animation

7. Writing Custom Layouts

When none of the default OpenLaszlo layouts do what you need, you can write a custom layout of your own. This section shows the essential aspects of building one.

7.1. The update() method

The update() method is the most important method in a layout — it controls what the layout does. For example, here is a very basic custom layout that spaces views 20px apart. This is similar to the behavior of the<simplelayout>, but unlike <simplelayout> this one spaces the left hand edge of the views, rather than distributing them evenly:

Example 17.17. Updating a layout

<canvas height="100" width="100%">
  <class name="col" clickable="true" bgcolor="blue" width="15" height="80"/>

  <layout>
    <attribute name="spacing" value="25" type="number"/>
    <method name="addSubview" args="s">
      super.addSubview(s);
      this.update();
    </method>

    <method name="update" args="e=null"><![CDATA[
        
      if (this.locked) return;
      this.locked = true;
      for (var i = 0; i < subviews.length; i++){
          subviews[i].setAttribute('x',  i * this.spacing);
      }
      this.locked= false;
      
    ]]></method>
  </layout>

  <col/>
  <col/>
  <col/>
  <col/>
</canvas>

All layouts have a subviews array, which allows you to cycle through the sibling views and adjust their properties accordingly. The addSubview() method is automatically called whenever a view is added—at init time, when a view is dynamically generated from script or via data replication. It ensures that the update() method gets called so that the layout updates. If the addSubview()method were not present, the above example would still look fine when the application first started, but any dynamically created views would appear out of the layout.

Notice both:

  1. The test for the locked attribute (if (this.locked) return;) to ensure conformance with normal layout response to the locking and unlocking of layouts.

  2. The use of the locked attribute in the update() method (this.locked = true; and this.locked = false;) to prevent simultaneous changes to the layout.

Here is a slightly more complicated example. In fact the layout defined below is the same as <simplelayout axis="x" spacing="10"/>.

Example 17.18. Horizontal simplelayout

<canvas height="100" width="100%">
  <class name="col" clickable="true" bgcolor="blue" width="20" height="80" onclick="this.setAttribute('width', this.width+2)"/>
  <layout>
    <attribute name="spacing" value="10" type="number"/>
    <method name="addSubview" args="s">
      this.updateDelegate.register(s, "onwidth");
      super.addSubview(s);
      this.update();
    </method>
    <method name="update" args="e=null"><![CDATA[
        
      if (this.locked) return;
      this.locked = true;
      var indent = 0;
      for (var i = 0; i < subviews.length; i++){
          var s = subviews[i];
          s.setAttribute('x',  indent);
          indent += s.width + this.spacing;
      }
      this.locked = false;
      
    ]]></method>
  </layout>

  <col/>
  <col/>
  <col/>
  <col/>
  <col/>
  <col/>
</canvas>

Notice that the update() method is slightly different now — it adds up the total widths of the preceding siblings (adding the spacing) and sets the x to the total. That is the basic algorithm for simplelayout. Another very important difference is that the addSubview() method registers the inherited updateDelegate() to listen for changes to that subview's width. updateDelegate() is a delegate that all subclasses of layout have. It calls that layout's update() method. In this example, the layout needs to update when the width of any of the sibling views changes, so the call to register it happens in the addSubview() method.