Showing posts with label widget. Show all posts
Showing posts with label widget. Show all posts

Sunday, January 23, 2011

GWT DialogBox with close widget inside the header

I was looking in internet for dialog box which allows me to put widget inside the header but most of the examples I found were not really very helpful. I collect the experience in Internet and create my own widget. It is not really the best approach but works very well for now.

1 /*
2 * Copyright 2010
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 package com.mvp.client.ui.widget;
17
18 import com.google.gwt.core.client.GWT;
19 import com.google.gwt.dom.client.EventTarget;
20 import com.google.gwt.dom.client.NativeEvent;
21 import com.google.gwt.safehtml.shared.SafeHtml;
22 import com.google.gwt.user.client.Element;
23 import com.google.gwt.user.client.Event;
24 import com.google.gwt.user.client.ui.DialogBox;
25 import com.google.gwt.user.client.ui.HTML;
26 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
27 import com.google.gwt.user.client.ui.HorizontalPanel;
28 import com.google.gwt.user.client.ui.Widget;
29
30 /**
31 * Extended DialogBox widget with close button inside the pop-up header
32 *
33 * @author L.Pelov
34 */
35 public class DialogBoxExt extends DialogBox {
36
37 private HorizontalPanel captionPanel = new HorizontalPanel();
38
39 // widget which will be use to close the dialog box
40 private Widget closeWidget = null;
41
42 /**
43 * You have to provide a widget here. If click on the widget the dialog box
44 * will be closed.
45 *
46 * @param closeDialogBox
47 */
48 public DialogBoxExt(Widget closeDialogBox) {
49 super();
50
51 closeWidget = closeDialogBox;
52
53 // empty header could case a problem!
54 setHTML(" ");
55 }
56
57 @Override
58 public void setHTML(String html) {
59 if (closeWidget != null) {
60 setCaption(html, closeWidget);
61 } else {
62 super.setHTML(html);
63 }
64 }
65
66 @Override
67 public void setHTML(SafeHtml html) {
68 if (closeWidget != null) {
69 setCaption(html.asString(), closeWidget);
70 } else {
71 super.setHTML(html);
72 }
73 }
74
75 /**
76 * Makes a new caption and replace the old one.
77 *
78 * @param txt
79 * @param w
80 */
81 private void setCaption(String txt, Widget w) {
82 captionPanel.setWidth("100%");
83 captionPanel.add(new HTML(txt));
84 captionPanel.add(w);
85 captionPanel.setCellHorizontalAlignment(w,
86 HasHorizontalAlignment.ALIGN_RIGHT);
87 // make sure that only when you click on this icon the widget will be
88 // closed!, don't make the field too width
89 captionPanel.setCellWidth(w, "1%");
90 captionPanel.addStyleName("Caption");
91
92 // Get the cell element that holds the caption
93 Element td = getCellElement(0, 1);
94
95 // Remove the old caption
96 td.setInnerHTML("");
97
98 // append our horizontal panel
99 td.appendChild(captionPanel.getElement());
100 }
101
102 /**
103 * Close handler, which will hide the dialog box
104 */
105 private class DialogBoxCloseHandler {
106 public void onClick(Event event) {
107 hide();
108 }
109 }
110
111 /**
112 * Function checks if the browser event is was inside the caption region
113 *
114 * @param event
115 * browser event
116 * @return true if event inside the caption panel (DialogBox header)
117 */
118 protected boolean isCaptionControlEvent(NativeEvent event) {
119 // return isWidgetEvent(event, captionPanel.getWidget(1));
120 return isWidgetEvent(event, closeWidget);
121 }
122
123 /**
124 * Overrides the browser event from the DialogBox
125 */
126 @Override
127 public void onBrowserEvent(Event event) {
128 if (isCaptionControlEvent(event)) {
129
130 switch (event.getTypeInt()) {
131 case Event.ONMOUSEUP:
132 case Event.ONCLICK:
133 new DialogBoxCloseHandler().onClick(event);
134 break;
135 case Event.ONMOUSEOVER:
136 break;
137 case Event.ONMOUSEOUT:
138 break;
139 }
140
141 return;
142 }
143
144 // go to the DialogBox browser event
145 super.onBrowserEvent(event);
146 }
147
148 /**
149 * Function checks if event was inside a given widget
150 *
151 * @param event
152 * - current event
153 * @param w
154 * - widget to prove if event was inside
155 * @return - true if event inside the given widget
156 */
157 protected boolean isWidgetEvent(NativeEvent event, Widget w) {
158 EventTarget target = event.getEventTarget();
159
160 if (Element.is(target)) {
161 boolean t = w.getElement().isOrHasChild(Element.as(target));
162 GWT.log("isWidgetEvent:" + w + ':' + target + ':' + t);
163 return t;
164 }
165 return false;
166 }
167
168 }
169

You can checkout the code from here.


Let’s see the code in details. I am overriding directly the DialogBox widget, I don’t really want to change the original code. Everything inside works very well, I just add a new functionality to the existing one.


The new constructor takes now a widget element. This element could be any GWT widget. You can use a picture or just a html text. Inside the constructor I call the function setHTML once to make sure that the header is not empty. Empty header could cause some problems, when you try to catch the events.The function setHTML is overridden. If the widget which should represent a closing element inside the header is not available, then everything works like before. You can use this overridden class in your code without to worry of already implemented functionality.


The next step will be to replace the caption header from the original DialogBox with our widget. This is done in the function setCaption. Basically what the function does, it generates caption which is now a HorizontalPanel widget, then remove the original caption widget and then appends new child element instead which is our already generated header. The following line code: captionPanel.setCellWidth(w, "1%"); guaranties that the icon or the text you use to close the dialog box is not too bright.


The next part is to override the onBrowserEvent function. Basically what we have to do here is to check if there is onClick event inside the browser and if the click was made on our close widget element. If so then hide the panel, otherwise just go to the native dialog box event handling.


cheers

Tuesday, November 9, 2010

GWT CellTable with sorting

UPDATE: There is new article about the sorting here.

The latest version of GWT (version 2.1) brings some new widget possibilities for building UI. One of those components is the Date Presentation Widgets which gives you a set of powerful and high performance components like lists, table, trees and browsers. In this blog I would like to show you hot to implement sorting in CellTable Data Presentation Widget. CellTable basically renders row values in columns but at the current version does not support sorting out-of-box. I was looking after how to implement this functionality and realize that there is already very good example in the Expenses demo application which you will find in the GWT Installation Samples folder. I tried to get out only the sorting functionality and implement it in my own CellTable and here I would like to give you my experience.

First you need to implement header which handles click operation. In my example I’ve just got the one implemented into the Expenses example.

1 /*
2 * Copyright 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 package com.mvp.client.ui;
17
18 import com.google.gwt.cell.client.ClickableTextCell;
19 import com.google.gwt.core.client.GWT;
20 import com.google.gwt.resources.client.ClientBundle;
21 import com.google.gwt.resources.client.ImageResource;
22 import com.google.gwt.safehtml.client.SafeHtmlTemplates;
23 import com.google.gwt.safehtml.shared.SafeHtml;
24 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
25 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
26 import com.google.gwt.user.cellview.client.Header;
27 import com.google.gwt.user.client.ui.AbstractImagePrototype;
28
29 public class SortableHeader extends Header<String> {
30
31 interface Template extends SafeHtmlTemplates {
32 @Template("<div style=\"position:relative;cursor:hand;cursor:pointer;"
33 + "padding-right:{0}px;\">{1}<div>{2}</div></div>")
34 SafeHtml sorted(int imageWidth, SafeHtml arrow, String text);
35
36 @Template("<div style=\"position:relative;cursor:hand;cursor:pointer;"
37 + "padding-right:{0}px;\"><div style=\"position:absolute;display:none;"
38 + "\"></div><div>{1}</div></div>")
39 SafeHtml unsorted(int imageWidth, String text);
40 }
41
42 private static Template template;
43
44 /**
45 * Image resources.
46 */
47 public static interface Resources extends ClientBundle {
48
49 ImageResource downArrow();
50
51 ImageResource upArrow();
52 }
53
54 private static final Resources RESOURCES = GWT.create(Resources.class);
55 private static final int IMAGE_WIDTH = 16;
56 private static final SafeHtml DOWN_ARROW = makeImage(RESOURCES.downArrow());
57 private static final SafeHtml UP_ARROW = makeImage(RESOURCES.upArrow());
58
59 private static SafeHtml makeImage(ImageResource resource) {
60 AbstractImagePrototype proto = AbstractImagePrototype.create(resource);
61 String html = proto.getHTML().replace("style='",
62 "style='position:absolute;right:0px;top:0px;");
63 return SafeHtmlUtils.fromTrustedString(html);
64 }
65
66 private boolean reverseSort = false;
67 private boolean sorted = false;
68 private String text;
69
70 public SortableHeader(String text) {
71 super(new ClickableTextCell());
72 if (template == null) {
73 template = GWT.create(Template.class);
74 }
75 this.text = text;
76 }
77
78 public boolean getReverseSort() {
79 return reverseSort;
80 }
81
82 @Override
83 public String getValue() {
84 return text;
85 }
86
87 @Override
88 public void render(SafeHtmlBuilder sb) {
89 if (sorted) {
90 sb.append(template.sorted(IMAGE_WIDTH, reverseSort ? DOWN_ARROW
91 : UP_ARROW, text));
92 } else {
93 sb.append(template.unsorted(IMAGE_WIDTH, text));
94 }
95 }
96
97 public void setReverseSort(boolean reverseSort) {
98 this.reverseSort = reverseSort;
99 }
100
101 public void setSorted(boolean sorted) {
102 this.sorted = sorted;
103 }
104
105 public void toggleReverseSort() {
106 this.reverseSort = !this.reverseSort;
107 }
108 }
109

Take a look at the render method. This methods is called every time when the CellTable renders the headers. In the example above you will see that an arrow image will be rendered every time together with the text depending on the sorting (ASC or DESC). Also in the constructor you will see that our header is based on a ClickableTextCell. This cell implementation calls the ValueUpdater implementation every time you click on the cell. Make sure when you copy this code to your project to get also the two images downArrow.jpg and upArrow.jpg which are in the same folder together with the SortableHeader class in Expenses example.


Next step is to get the generic interface which says how the cell value will be returned. The interface is very simple and self explained. You will pass to the cell an object T and return object C.


1 public interface GetValue<T, C> {
2 C getValue(T object);
3 }
4 private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();

The interfaces does not need to know the type of the values now. You can do it even more complicated, for example the object T could implement specific interface. The variable allHeaders contains all sortable headers. We will used this like shown Expenses example to modify the value of the sort before we re-render the headers, if you remember the render method from the SortableHeaders class.


As next I used the object ContactInfo from the CellTable example, which you can find here. This calls could be any shared object you want to transport via RPC. Don’t forget to implement interface Comparable which you will need for the sorting later. Here the ContactInfo from the ContactDatabase.java from the CellTable example:


1 public static class Category {
2
3 private final String displayName;
4
5 private Category(String displayName) {
6 this.displayName = displayName;
7 }
8
9 public String getDisplayName() {
10 return displayName;
11 }
12 }
13
14 public static class ContactInfo implements Comparable<ContactInfo> {
15
16 /**
17 * The key provider that provides the unique ID of a contact.
18 */
19 public static final ProvidesKey<ContactDatabase.ContactInfo> KEY_PROVIDER = new ProvidesKey<ContactInfo>() {
20 public Object getKey(ContactInfo item) {
21 return item == null ? null : item.getId();
22 }
23 };
24
25 private static int nextId = 0;
26
27 private String address;
28 private Date birthday;
29 private Category category;
30 private String firstName;
31 private final int id;
32 private String lastName;
33
34 public ContactInfo(Category category) {
35 this.id = nextId;
36 nextId++;
37 setCategory(category);
38 }
39
40 @Override
41 public int compareTo(ContactInfo o) {
42 return (o == null || o.firstName == null) ? 0 : o.firstName
43 .compareTo(firstName);
44 }
45
46 @Override
47 public boolean equals(Object o) {
48 if (o instanceof ContactInfo) {
49 return id == ((ContactInfo) o).id;
50 }
51 return false;
52 }
53
54 /**
55 * @return the contact's address
56 */
57 public String getAddress() {
58 return address;
59 }
60
61 /**
62 * @return the contact's birthday
63 */
64 public Date getBirthday() {
65 return birthday;
66 }
67
68 /**
69 * @return the category of the conteact
70 */
71 public Category getCategory() {
72 return category;
73 }
74
75 /**
76 * @return the contact's firstName
77 */
78 public String getFirstName() {
79 return firstName;
80 }
81
82 /**
83 * @return the contact's full name
84 */
85 public final String getFullName() {
86 return firstName + " " + lastName;
87 }
88
89 /**
90 * @return the unique ID of the contact
91 */
92 public int getId() {
93 return this.id;
94 }
95
96 /**
97 * @return the contact's lastName
98 */
99 public String getLastName() {
100 return lastName;
101 }
102
103 @Override
104 public int hashCode() {
105 return id;
106 }
107
108 /**
109 * Set the contact's address.
110 *
111 * @param address
112 * the address
113 */
114 public void setAddress(String address) {
115 this.address = address;
116 }
117
118 /**
119 * Set the contact's birthday.
120 *
121 * @param birthday
122 * the birthday
123 */
124 public void setBirthday(Date birthday) {
125 this.birthday = birthday;
126 }
127
128 /**
129 * Set the contact's category.
130 *
131 * @param category
132 * the category to set
133 */
134 public void setCategory(Category category) {
135 assert category != null : "category cannot be null";
136 this.category = category;
137 }
138
139 /**
140 * Set the contact's first name.
141 *
142 * @param firstName
143 * the firstName to set
144 */
145 public void setFirstName(String firstName) {
146 this.firstName = firstName;
147 }
148
149 /**
150 * Set the contact's last name.
151 *
152 * @param lastName
153 * the lastName to set
154 */
155 public void setLastName(String lastName) {
156 this.lastName = lastName;
157 }
158 }

Now we come to the more interesting part. The method which handles the logic with the adding the SortableHeaders into the CellTable. I’ve got this method from the Expenses example but I make it a little bit simpler and accommodate it to my needs, so here is it:


1 private <C> Column<ContactInfo, C> addColumn(final String text,
2 final Cell<C> cell, final GetValue<ContactInfo, C> getter,
3 final Comparator<ContactInfo> ascComparator,
4 final Comparator<ContactInfo> descComparator) {
5
6 // gets the cell value
7 final Column<ContactInfo, C> column = new Column<ContactInfo, C>(cell) {
8 @Override
9 public C getValue(ContactInfo object) {
10 return getter.getValue(object);
11 }
12 };
13
14 final SortableHeader header = new SortableHeader(text);
15 allHeaders.add(header);
16
17 // call this everytime headers is clicked
18 header.setUpdater(new ValueUpdater<String>() {
19 public void update(String value) {
20 header.setSorted(true);
21 header.toggleReverseSort();
22
23 for (SortableHeader otherHeader : allHeaders) {
24 if (otherHeader != header) {
25 otherHeader.setSorted(false);
26 otherHeader.setReverseSort(true);
27 }
28 }
29
30 // sort the clicked column
31 sortExpenses(ContactDatabase.get().getDataProvider().getList(),
32 header.getReverseSort() ? descComparator
33 : ascComparator);
34
35 cellTable.redrawHeaders();
36
37 // Go to the first page of the newly-sorted results, if wished
38 // pager.firstPage();
39 }
40 });
41 cellTable.addColumn(column, header);
42 return column;
43 }
44

What we have here is addColumn method which adds given a name and cell to the our CellTable object. For SortableHeader we set also ValueUpdater which will be called every time the header has been clicked and then inside we call the function sortExpenses which does the actual sorting. This function gets the comparator implementation and depending on it sorts the column. The comparator implementation looks like this:


1 private <C extends Comparable<C>> Comparator<ContactInfo> createColumnComparator(
2 final GetValue<ContactInfo, C> getter, final boolean descending) {
3 return new Comparator<ContactInfo>() {
4 public int compare(ContactInfo o1, ContactInfo o2) {
5 // Null check the row object.
6 if (o1 == null && o2 == null) {
7 return 0;
8 } else if (o1 == null) {
9 return descending ? 1 : -1;
10 } else if (o2 == null) {
11 return descending ? -1 : 1;
12 }
13
14 // Compare the column value.
15 C c1 = getter.getValue(o1);
16 C c2 = getter.getValue(o2);
17 if (c1 == null && c2 == null) {
18 return 0;
19 } else if (c1 == null) {
20 return descending ? 1 : -1;
21 } else if (c2 == null) {
22 return descending ? -1 : 1;
23 }
24 int comparison = c1.compareTo(c2);
25 return descending ? -comparison : comparison;
26 }
27 };
28 }

As you can see the comparator gets the GetValue implementation which could be any column. This allows you to pass any parameter from ContactInfo without the needs to implement new comparator for every column element.


Now you can add new columns to your CellTable like this:


1 addColumn("First name", new TextCell(),
2 new GetValue<ContactInfo, String>() {
3 public String getValue(ContactInfo object) {
4 return object.getFirstName();
5 }
6 });

If you want to check out the full example I share it here.