Window Tutorial
Introduction
FIXME:
Put in links to Peter's components guide
This tutorial shows you how to make a window for enclosing and controlling input and display.
Designing the window visually
Before we begin to code, let's consider what a window is. Essentially it's a collection of views, some of which are flexible in size, and others fixed. To break it down even more, think of it as three rows:
In the top row, the left and right corners remain a fixed size, while the center bar stretches to fill the width of the window.
In the body area, this pattern is repeated, only the left and right edges are much thinner than the left and right corners of the top row.
Again, this pattern is repeated for the bottom row.
The following diagram should make this clear.
Now let's look at the positioning of the elements. As we shall see, this step is not necessary, because Laszlo provides a shortcut, but it will help to understand views.
In the top row, the fist element (the "Media" part) is located at x=0 (the left hand edge). So when does the second element start (the part that stretches)? It's x-coordinate is going to be the width of the first element. And the x-coordinate of the third element is the width of itself from the right hand edge of the window.
<canvas width="800" height="600">
<view name="mediaWindow"
x="10" y="10"
width="200" height="150"
clickable="true">
<simplelayout axis="y"/>
<view name="topRow">
<view name="topLeft"/>
<view name="topCenter"/>
<view name="topRight"/>
</view>
<view name="body">
<view name="leftSide"/>
<view name="content"/>
<view name="rightSide"/>
</view>
<view name="bottomRow">
<view name="bottomLeft"/>
<view name="bottomCenter"/>
<view name="bottomRight"/>
</view>
</view>
</canvas>
Not surprisingly, there is nothing to see in the output. We haven't added any art assets yet. You can use the ones we supply, or feel free to design your own.
Notice how we made the whole window clickable? That's because if we made only some parts of the window clickable, clicks would go through the window, and could register with something visually below it. The simplelayout<simplelayout>[unknown tag]
tag ensures that the rows will stack vertically.
Using <simplelayout> to position art
OK that doesn't look great, but the art assets are all in there, the views are named, so we're ready to go!
Recall that we said that the x-coordinate of the topCenter view was the width of the topLeft view? We're going to use that principle right in the x-attributes of the views.
Setting x attributes
One thing we forgot about was the change the y value for the topCenter and topRight views: They are both shorter than the first one (by 10px), so we can just give them both a y-value of 10. We could be really clever and give them a dynamic y-value, but that's not really necessary here.
Since the center view is variable width, we needed to allow it to stretch its resource, by adding the stretches="width" attribute to the view. We won't see any results from this right now, but when we move on to resizing the window, the effect will become apparent.
We also had to set its width to be dependant on the widths of its neighboring two views, and that of its parent. There is no need to specify the widths of topLeft or topRight, as they shall take on the widths of their resources.
Stretchable widths
That's fixed now.
The system we used to constrain the elements of the top of the view together is not actually necessary in this case. The idea of three neighboring views, where the outer two are fixed width, and the center one stretches is very common, so Laszlo allows for a shorthand way of doing that. For the body and bottom row, we shall be using this shorthand system, but you are encouraged to use it whenever possible, because it is faster.
Constraining widths
The shorthand method in question is the tag stableborderlayout. It takes one attribute, axisaxis which determines which way to space the elements. It has an optional spacing attribute too, but we're not using that right now.
Note that it is recommended to put layouts (simpleborderlayoutsimpleborderlayout and stableborderlayoutstableborderlayout) at the end of the view tags they affect.
Our window appears too big, even though we've set its size in the view<view>[unknown tag]
tag. This is because we haven't yet told it to stretch (or in this particular case, compress) the art assets.
Completing the layout
We had to set the views to stretch, and set their heights to their parent view, in this case, body. The body height is now governed by the stableborderlayout<stableborderlayout>[unknown tag]
tag, which replaces the old simplelayout<simplelayout> tag. Note that its axis is now set to y.
Making the window draggable.
For the purposes of this exercise, we are going to make this window draggable by its menubar; just like the window<window>[unknown tag]
component.
It's important to understand mouse events for this. When something is dragged, two mouse events happen: First the mouse is pressed down (not clicked ??? clicked is down, then up), second the mouse is lifted up. So in short what we shall be doing is listening for the mouse to go down (on the menu), at which point we will move the window relative to the current mouse position. Then when we hear that the mouse has gone back up, we will stop repositioning the window.
Making window Draggable
We're using the dragstate<dragstate>[unknown tag]
component here. This is an extension of a LZX concept we have not covered yet, namely a state. A state is a predefined set of instructions as to how a view behaves when it is applied. The dragstate<dragstate> tag ensures that the view follows the mouse. Notice how the mediaWindow view has the dragstate (because the whole thing is being dragged), and not the menu bar.
Making the window resizable.
We're going to make this window resizable by its bottom-right hand corner. (For this example, we're not doing the full job of making the window resize by grabbing any edge. When the mouse is pressed, that's when we're going to start taking readings at regular intervals, and adjusting the size of the window accordingly. When the mouse is lifted back up, that's when we're going to stop adjusting the size of the window.
We shall need three methods:
startResize()startResize()called when the mouse is pressed down (onmousedown eventonmousedown event).adjustSize()adjustSize()called repeatedly when the mouse is depressed. To achieve this idle repetition, we will create a delegate that is called every onidle eventonidle ??? every frame.stopResize()stopResize()called when the mouse is lifted up (onmouseup eventonmouseup event).
The delegate will be set in startResize()startResize(). The new
dimensions of the window are dependant on the distance of the mouse
from the top-right corner of the window. However we need to include an
offset too:
In the diagram, at position 1, the user presses down on the mouse. So at position 2, the width of the window will be set to the distance from the origin of the window view, or newX. Remember that newX is relative to the origin of the window view, not the canvas. However when the mouse is depressed, it's not exactly at the edge of the window - it's a few pixels in from the right hand edge (offsetX). So if we fail to add those few pixels to the new width, the corner of the window will be getting dragged by the very edge (as shown in the diagram). This would feel odd. The same goes for the y-axis.
Therefore we need to record the offsets (x and y) when the mouse is depressed. We can't just create variables, because the method that will handle the resizing (adjustSize()adjustSize(), see above) will not be the same as the method that is called when the mouse is depressed. So the solution is to assign the offsets as properties of the window every time the window is set.
Using <dragstate>
First we assign the offset properties to the window. Next, we create the delegate (testing first to make sure it does not already exist). The delegate becomes a attribute of the window (this.sizerDel = new...). When we create a delegate, we have to pass it the context (this), the method to call (adjustSize()adjustSize()), the event sender (LzIdlelz.Idle) and the event name (onidle eventonidle).
This translates to something like: "Every time the LzIdlelz.Idle service publishes an
onidle eventonidle event, call the adjustSize()adjustSize() method as if it were called from this".
Later, we clear this delegate by calling the unregisterAll()unregisterAll() method, and setting the delegate to null, so that it will evaluate to false next time we do the if (!this.sizerDel) test.
The window resizes smoothly, but as you can see, strange things can happen when the window is resized quickly. This is because the sizes of some of the elements get set to a negative value, causing our window to break. Also, it's possible to completely invert the window.
The simple solution is to have a minimum height and width for the window. The way to do this is to set attributes of the window tag, and then test to see if the window should still be resized.
<canvas width="800" height="600" debug="true">
<view name="mediaWindow"
x="10" y="10"
width="200" height="150"
clickable="true">
<attribute name="minWindowWidth" value="150"/>
<attribute name="minWindowHeight" value="60"/>
... Top two rows removed for clarity ...
<method name="adjustSize">
<![CDATA[
var newWidth = this.getMouse('x') + this.resizeOffsetX;
var newHeight = this.getMouse('y') + this.resizeOffsetY;
if (newWidth > this.minWindowWidth) {
this.setAttribute('width', newWidth);
}
if (newHeight > this.minWindowHeight) {
this.setAttribute('height', newHeight);
}
]]>
</method>
<method name="stopResize">
this.sizerDel.unregisterAll();
this.sizerDel = null;
</method>
</view>
</canvas>
The key differences here are that we've added the attribute tags, and the change to the adjustSize()adjustSize() method that checks to see that the window is not smaller than it should be. Note the use ofCDATA, which allow the use of > and < in the method.
Just like the dragstate<dragstate> tag, LZX has a resizestate<resizestate>[unknown tag]
component that makes resizing views a breeze. The reason we didn't use it here was to show how you can have manual control over views using delegates, and also because in order to achieve minimum/maximum constraints we would have to overwrite the resizestate<resizestate> class, which introduces too many concepts in one go.
Reusing the window (turning it into a class)
Should we need more windows like this one, we could of course write all new view tags for them, but that would take forever, and is completely unnecessary. We can reuse the common features of this window instead, by changing it from a view to a class.
So what needs to be changed?
view name="mediaWindow" ...
will change to
class name="mediaWindow" ...
And in future, the name mediaWindow will be used to instantiate the class.
'parent' addressing
So far we've been using parent to mean the view that's superior to the current one but with classes, this changes.
<class name="mediaWindow">
<view name="myView"/>
</class>
<mediaWindow name="myInstance">
<view name="myPlacedView" placement="myView"/>
</mediaWindow>
In the snippet of code above, we have defined the mediaWindowmediaWindow class, then instantiated it. This means that myInstance contains a view named myView. When we instantiate mediaWindowmediaWindow, we can pass it a view, and tell that view where to go. That's exactly what we're doing with myPlacedView. myPlacedView's parent is the mediaWindowmediaWindow's myView, whereas its immediateparent is myInstance.
Completed window
As firstWindow clearly shows, it is possible to override the attributes (such as widthwidth and heightheight, that were specified in the class definition).
What secondWindow shows is that we can override our own attributes (such as minWindowWidthminWindowWidth and minWindowHeightminWindowHeight) too. Also, you can see that we have created some text (which is really a view) and given it a placementplacement attribute to tell it that it should go in the content view as defined in the class definition. This is where it is important to remember the difference between parentparent and immediateparentimmediateparent.