May 16 2008

Gwt-Ext and Number Madness

Tag: Ext, Gwt, Java, Javascript, Programming, WebAbhijeet Maharana @ 11:52 pm

Gwt-Ext Number Madness screenshotThis is the 3rd form of my game Number madness. I had first written it using TurboC during the vacation after junior college. And then as a Firefox extension. Both are available in the projects section.

Using Gwt-Ext for writing something like this sounds a bit silly. But it did make me learn something new: button templates; thanks to gwtext+ and sjivan. I have described the program below in brief. You can download Eclipse project from the link at the end of this post.

An Ext button is made up of a table with 3 columns. All three columns have parts of the button sprite as their background image and the center TD holds a <button> element. You can see this template in Button.java above the setTemplate() method definition:

1
2
3
4
5
6
7
8
9
10
<table border="0" cellpadding="0" cellspacing="0" class="x-btn-wrap">
	<tbody>
		<tr>
	     		<td class="x-btn-left"><i>&#160;</i></td>
	     		<td class="x-btn-center">
	     			<em unselectable="on"><button class="x-btn-text" type="{1}">{0}</button></em></td>
	     		<td class="x-btn-right"><i>&#160;</i></td>
	     	</tr>
	</tbody>
</table>

We can override this template to make the button look as we want it to. My modified template looks as shown below. There is only one extra CSS class for the table (see notes at end):

1
2
3
4
5
6
7
8
9
10
<table border="0" cellpadding="0" cellspacing="0" class="x-btn-wrap mybutton">
	<tbody>
		<tr>
	     		<td class="x-btn-left"><i>&#160;</i></td>
	     		<td class="x-btn-center">
	     			<em unselectable="on"><button class="x-btn-text" type="{1}">{0}</button></em></td>
	     		<td class="x-btn-right"><i>&#160;</i></td>
	     	</tr>
	</tbody>
</table>

This enables us to use CSS nesting to define x-btn-left, x-btn-center, x-btn-right and x-btn-text with the attributes we want while preventing the normal buttons from being affected. Thus we can use both Ext’s buttons and out custom buttons at the same time. I have defined them as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.mybutton .x-btn-right {
	background: transparent url('button_right_06.png') no-repeat scroll right; 
	height: 150px;
	padding-right: 15px;
}
 
.mybutton .x-btn-left {
	background: transparent url('button_left_06.png') no-repeat scroll right; 
	height: 150px;
	padding-right: 15px;
}
 
.mybutton .x-btn-center {
	background: transparent url('button_06.png') repeat-x center; 
	height:150px;
	width:80px;
	text-align:center;
}
 
.mybutton .x-btn-text {
	line-height: 150px;
	font-family: Arial, Helvetica, sans-serif;
	font-size:60px;
	font-weight:bold;
}

I have used custom images that make up the button’s left, center and right components. I have also changed the appearance of the text displayed on the button.

Now that we have the buttons which make up the game board, lets take a brief look at the rest of the game. An integer array holds the numbers that map to each button on the board. This array is populated randomly at start. Although the board is 2D, I decided to use a 1D array as it turned out to be slightly simpler while populating the random numbers. So arr2D[i][j] becomes arr1D[i * numcols + j] where numcols is the number of columns in each row.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void generateRandomNumbers()
{
	int i,j;
	int temp;
	boolean suitable;
	int total = numRows*numCols-1;
 
	for(i=0;i<=total;i++)
	{
		do
		{
			suitable = true;
			temp = (int) Math.round(Math.random() * total);
			for(j=0;j<i;j++)
			{
				if(numbers[j] == temp)
				{
					suitable = false;
					break;
				}
			}
		}while(!suitable);
 
		numbers[i] = temp;
	}
}

Once the array is ready, I set the Panel’s layout to TableLayout with 3 columns and call createGrid() to create buttons that make up the grid. Every button knows its (row,column) position and the blank square is made up of an invisible button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void createGrid()
{
	for(int i=0; i<numRows; ++i)
	{
		for(int j=0; j<numCols; ++j)
		{
			int index = i*numCols + j;
 
			String label = String.valueOf(numbers[i*numCols + j]);
			final Button btn = new Button(label);
			btn.setTemplate(buttonTemplate);
 
			// listener is an instance of SquareListener
			btn.addListener(listener);
 
			// every button keeps track of its row and column number
			final int finali = i, finalj=j;
			btn.addListener("render", new Function(){
				public void execute() {
					JavaScriptObjectHelper.setAttribute(btn.getJsObj(), "row", finali);
					JavaScriptObjectHelper.setAttribute(btn.getJsObj(), "col", finalj);
				}
			});
 
			// empty square = invisible button
			// maintain a class-level reference to the empty square
			if (numbers[index] == 0)
			{
				emptyButton = btn;
				btn.setVisible(false);
			}
 
			add(btn);
		}
	}
}

The button listener checks if the clicked button is adjacent to the empty square. If it is, it swaps numbers in the array and on the buttons. The invisible button is now made visible and clicked button becomes invisible. I have added a fade in and fade out effect. However, this is shaky and sometimes the button doesn’t fade in for too long. It does irritate a bit. After this, the endgame is checked i.e. if numbers in the array are in ascending order.

I have left out some code to keep the post short. You can download the complete project from Gwt-Ext.com or Rapidshare.

Note:

  1. Since our new template just has an extra CSS class, the whole template thing could be avoided by calling btn.setCls(“mybutton”). This was an after-thought when I was done playing with the template.
  2. When you change the template, keep the <button> element otherwise you will get an exception at load time
    1
    2
    3
    4
    5
    
    [ERROR] Unable to load module entry point class com.maharana.gwtextnumbermadness.client.MainModule (see associated exception for details)
    com.google.gwt.core.client.JavaScriptException: JavaScript TypeError exception: btnEl has no properties
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:481)
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNativeHandle(ModuleSpace.java:225)
    	...
  3. Also keep the x-btn-center CSS class name otherwise you will get an exception when the button is clicked
    1
    2
    3
    4
    5
    
    [WARN] Exception thrown into JavaScript
    com.google.gwt.core.client.JavaScriptException: JavaScript TypeError exception: this.el.child("td.x-btn-center " + this.buttonSelector) has no properties
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:481)
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNativeVoid(ModuleSpace.java:270)
    	...

References:

  1. Images for new button template: http://www.jankoatwarpspeed.com/post/2008/04/30/make-fancy-buttons-using-css-sliding-doors-technique.aspx
  2. CSS nesting: http://webdesignfromscratch.com/css-inheritance-cascade.cfm
  3. Centering block level elements: http://dorward.me.uk/www/centre/#content

Do let me know if you have any corrections or suggestions.


May 01 2008

Gwt-Ext and Google Maps – II (handle click)

Tag: Ext, Gwt, Java, Javascript, Maps, Programming, WebAbhijeet Maharana @ 5:54 pm

Lat and Lon info being shown in a message box
This post is related to my earlier post on Gwt-Ext and Google Maps. While browsing the Gwt-Ext forum, I came across this thread with a simple-looking question from Martin: How can I get the LatLonPoint from a map when a user clicks on the map ??

I looked at the available methods to see if I could figure this out. When I realized I wasn’t getting anywhere, I tried looking at the source to figure out what was going on. I found that Mapstraction does have a facility to register callback functions for events. Below code snippets are from mapstraction.js.

1
2
3
4
5
6
7
8
9
10
11
Mapstraction.prototype.addEventListener = function(type, func) {
    var listener = new Array();
    listener.push(func);
    listener.push(type);
    this.eventListeners.push(listener);
    switch (this.api) {
    case 'openlayers':
        this.maps[this.api].events.register(type, this, func);
        break;
    }
}

When the callback functions registered for ‘click’ event are invoked, they are supplied with a LatLonPoint instance with the latitude and longitude information of the location which was clicked. See line 4 below.

1
2
3
4
5
6
7
Mapstraction.prototype.clickHandler = function(lat, lon, me) {
    for (var i = 0; i < this.eventListeners.length; i++) {
        if (this.eventListeners[i][1] == 'click') {
            this.eventListeners[i][0](new LatLonPoint(lat, lon));
        }
    }
}

However, this argument is lost because of the way Gwt-Ext API exposes this functionality: MapPanel class registers a function with no parameters. Below code snippet is from MapPanel.java. Note the native method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addEventListener(final String event, final Function listener) {
	if (!mapRendered) {
		addListener(MAP_RENDERED_EVENT, new Function() {
			public void execute() {
				doAddEventListener(event, listener);
			}
		});
	} else {
		doAddEventListener(event, listener);
	}
}
 
private native void doAddEventListener(String event, Function listener) /*-{
        var map = this.@com.gwtext.client.widgets.map.MapPanel::mapJS;
        map.addEventListener(event, function() {
            listener.@com.gwtext.client.core.Function::execute()();
        });
}-*/;

The solution is to override these two methods. For that we need an interface with an execute() method that can accept arguments. I added an interface ‘OneArgFunction’ that does this. We need a proper fix for this so that we can handle more arguments. For now, a one-argument method will suffice.

1
2
3
4
5
package com.maharana.gwtextmaps.client;
 
public interface OneArgFunction {
	public void execute(com.google.gwt.core.client.JavaScriptObject arg);
}

In the overridden methods below, I register a function which accepts the LatLonPoint instance as parameter and hands it over to the execute() method for further processing. Then I invoke the overridden addEventListener() to register an event handler that places a new marker and centers the map on the clicked location. GoogleMap inherits from MapPanel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mapPanel = new GoogleMap() {
	public void addEventListener(final String event, final OneArgFunction listener) {
		if (!this.isRendered()) {
			addListener(MAP_RENDERED_EVENT, new Function() {
				public void execute() {
					doAddEventListener(event, listener);
				}
			});
		} else {
			doAddEventListener(event, listener);
		}
	}
 
	private native void doAddEventListener(String event, OneArgFunction listener) /*-{
	      	var map = this.@com.gwtext.client.widgets.map.MapPanel::mapJS;
	      	map.addEventListener(event, function(llp) {
	            	listener.@com.maharana.gwtextmaps.client.OneArgFunction::execute(Lcom/google/gwt/core/client/JavaScriptObject;)(llp);
	      	});
	}-*/;
 
	// constructor - attach event listener
	{
		addEventListener("click", new OneArgFunction(){
			public void execute(JavaScriptObject arg) {
				LatLonPoint llp = new LatLonPoint(arg);
				mapPanel.setCenterAndZoom(llp, mapPanel.getZoom());
				mapPanel.addMarker(new Marker(llp));
				MessageBox.alert("Clicked Location", "Lat: " + llp.getLat() + "<br>Lon: " + llp.getLon());
			}
		});
	}
};

This does the trick. I am not using any Google Maps specific code here so it should work for other providers as well. Do let me know if I have missed something obvious or got something wrong.

I have modified the demo I posted in my earlier blog entry to include this. You can download it from Gwt-Ext.com or Rapidshare.