Sunday, March 20, 2011

Custom GWT Clickable Cell with images

Few days ago I had to create a custom clickable cell for the CellTable. The issue was to create a cell which shows file names together with the appropriated icon. For example, if we have a *.doc file then we should show also the icon of the file, the same also for excel or pdf files. It should look like this:

image

How to proceed with this issue? I found the answer in some of the GWT examples and also some of the cell implementations from the GWT code. Since GWT is open source why not profit from it. Of course you have to play around a little bit but, it wasn’t so bad.

Basically the issue is to create a custom Cell. You do need two classes. First one defines how the cell will be rendered and implements the SafeHtmlRenderer<T> interface. The second one is the table cell implementation itself and you do have to implement the abstract class AbstractSafeHtmlCell<T>. You may have to be careful with abstract classes implementation of course, for example the AbstractSafeHtmlCell class was changed between GWT 2.1 and GWT 2.2 version. Also for the icons you do need your own client bundle.I call it DocIcons. This class contains all icons I am going to use. So now here the code for the render class:

1
2 import com.google.gwt.core.client.GWT;
3 import com.google.gwt.resources.client.ImageResource;
4 import com.google.gwt.safehtml.client.SafeHtmlTemplates;
5 import com.google.gwt.safehtml.shared.SafeHtml;
6 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
7 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
8 import com.google.gwt.text.shared.SafeHtmlRenderer;
9 import com.google.gwt.user.client.ui.AbstractImagePrototype;
10 import com.oracle.wci.portlet.client.img.icons.DocIcons;
11 import com.oracle.wci.portlet.client.prop.ContentServerItem;
12
13 public class FolderItemSafeHtmlRenderer implements
14 SafeHtmlRenderer<ContentServerItem> {
15
16 interface Template extends SafeHtmlTemplates {
17 @Template("<div class=\"FolderItemSafeHtmlRendererSorted"
18 + "\">{1}<div class=\"FolderItemSafeHtmlRendererSortedDivTag\">{2}</div></div>")
19 SafeHtml sorted(int imageWidth, SafeHtml arrow, String text);
20
21 @Template("<div class=\"FolderItemSafeHtmlRendererSorted2"
22 + "\">{1}<div>{2}</div></div>")
23 SafeHtml sorted2(int imageWidth, SafeHtml arrow, String text);
24
25 @Template("<div class=\"FolderItemSafeHtmlRendererUnsorted"
26 + "\"><div class=\"FolderItemSafeHtmlRendererUnsortedDivTag"
27 + "\"></div><div>{1}</div></div>")
28 SafeHtml unsorted(int imageWidth, String text);
29 }
30
31 private static FolderItemSafeHtmlRenderer instance;
32 private static Template template;
33
34 private static final DocIcons RESOURCES = DocIcons.RESOURCES;
35 private static final int IMAGE_WIDTH = 20;
36
37 private static final SafeHtml ICON_FOLDER = makeImage(RESOURCES.dir());
38 private static final SafeHtml ICON_DOC = makeImage(RESOURCES
39 .tree_icon_item());
40
41 private static final SafeHtml ICON_PDF = makeImage(RESOURCES.icon_doc_pdf());
42
43 private static final SafeHtml ICON_WORD = makeImage(RESOURCES
44 .icon_doc_word());
45
46 private static final SafeHtml ICON_EXCEL = makeImage(RESOURCES
47 .icon_doc_excel());
48
49 private static final SafeHtml ICON_PPT = makeImage(RESOURCES
50 .icon_doc_ppoint());
51
52 private static final SafeHtml ICON_VISIO = makeImage(RESOURCES
53 .icon_doc_visio());
54
55 private static final SafeHtml ICON_ZIP = makeImage(RESOURCES.iconDocZip16());
56
57 /**
58 * Make icons available as SafeHtml to be displayed inside the table
59 *
60 * @param resource
61 * @return
62 */
63 private static SafeHtml makeImage(ImageResource resource) {
64 AbstractImagePrototype proto = AbstractImagePrototype.create(resource);
65 String html = proto.getHTML().replace("style='",
66 "style='position:absolute;left:0px;top:0px;");
67 return SafeHtmlUtils.fromTrustedString(html);
68 }
69
70 public static FolderItemSafeHtmlRenderer getInstance() {
71 if (instance == null) {
72 instance = new FolderItemSafeHtmlRenderer();
73 }
74
75 if (template == null) {
76 template = GWT.create(Template.class);
77 }
78
79 return instance;
80 }
81
82 private FolderItemSafeHtmlRenderer() {
83 }
84
85 @Override
86 public SafeHtml render(ContentServerItem object) {
87
88 if (object.isFolder()) {
89 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_FOLDER,
90 object.getName());
91 return str;
92 } else {
93
94 String docExt = object.getExtention();
95
96 if (docExt.equalsIgnoreCase("doc")) {
97 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_WORD,
98 object.getName());
99 return str;
100 } else if (docExt.equalsIgnoreCase("pdf")) {
101 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_PDF,
102 object.getName());
103 return str;
104 } else if (docExt.equalsIgnoreCase("docx")) {
105 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_WORD,
106 object.getName());
107 return str;
108 } else if (docExt.equalsIgnoreCase("ppt")) {
109 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_PPT,
110 object.getName());
111 return str;
112 } else if (docExt.equalsIgnoreCase("xls")) {
113 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_EXCEL,
114 object.getName());
115 return str;
116 } else if (docExt.equalsIgnoreCase("vsd")
117 || docExt.equalsIgnoreCase("vst")) {
118 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_VISIO,
119 object.getName());
120 return str;
121 } else if (docExt.equalsIgnoreCase("zip")
122 || docExt.equalsIgnoreCase("rar")) {
123 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_ZIP,
124 object.getName());
125 return str;
126 } else {
127 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_DOC,
128 object.getName());
129
130 return str;
131 }
132 }
133 }
134
135 @Override
136 public void render(ContentServerItem object, SafeHtmlBuilder appendable) {
137 if (object.isFolder()) {
138 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_FOLDER,
139 object.getName());
140
141 appendable.append(str);
142 } else {
143
144 SafeHtml str = template.sorted(IMAGE_WIDTH, ICON_DOC,
145 object.getName());
146
147 appendable.append(str);
148 }
149 }
150 }
151

It does not look really very special, but it works. The SafeHtmlTemplates are doing really good. What you have to take create about is that they do not support style attribute, so you have to replace it with class when you do want to pass CSS to the html inside the template, otherwise you will get same warning messages when you compile your code. Here is CSS I used:


1 .FolderItemSafeHtmlRendererSorted {
2 position:relative;
3 cursor:hand;
4 cursor:pointer;
5 padding-right:20px;
6 }
7 .FolderItemSafeHtmlRendererSortedDivTag {
8 padding-left:25px;padding-top:2px;
9 }
10 .FolderItemSafeHtmlRendererSorted2 {
11 position:relative;
12 cursor:hand;
13 cursor:pointer;
14 padding-right:20px;
15 }
16 .FolderItemSafeHtmlRendererUnsorted {
17 position:relative;
18 cursor:hand;
19 cursor:pointer;
20 padding-right:20px;
21 }
22 .FolderItemSafeHtmlRendererUnsortedDivTag {
23 padding-left:25px;padding-top:2px;
24 }

It is not the best example of using CSS, I promise next time I will do it better! The style is the same like the one used in the GWT Expenses examples when you check the code for the custom sortable header and it works here as well. The ContentServerItem is custom class contains the data passed to CellTable. I do have also a method inside which allows me to check if the object is folder or file, since for folder we do want to use different icon.


Now the class which implements the abstract cell:


1 import com.google.gwt.cell.client.AbstractSafeHtmlCell;
2 import com.google.gwt.cell.client.ValueUpdater;
3 import com.google.gwt.dom.client.Element;
4 import com.google.gwt.dom.client.NativeEvent;
5 import com.google.gwt.safehtml.shared.SafeHtml;
6 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
7 import com.google.gwt.text.shared.SafeHtmlRenderer;
8 import com.oracle.wci.portlet.client.prop.ContentServerItem;
9
10 public class FolderItemClickableTextCell extends AbstractSafeHtmlCell<ContentServerItem> {
11
12 private ValueUpdater<ContentServerItem> updater;
13
14 /**
15 * Construct a new ClickableTextCell that will use a {@link SimpleSafeHtmlRenderer}.
16 */
17 public FolderItemClickableTextCell() {
18 this(FolderItemSafeHtmlRenderer.getInstance());
19 }
20
21 /**
22 * Construct a new ClickableTextCell that will use a given {@link SafeHtmlRenderer}.
23 *
24 * @param renderer
25 * a {@link SafeHtmlRenderer SafeHtmlRenderer<String>} instance
26 */
27 public FolderItemClickableTextCell(SafeHtmlRenderer<ContentServerItem> renderer) {
28 super(renderer, "click", "keydown");
29 }
30
31
32 @Override
33 public void onBrowserEvent(com.google.gwt.cell.client.Cell.Context context,
34 Element parent, ContentServerItem value, NativeEvent event,
35 ValueUpdater<ContentServerItem> valueUpdater) {
36
37 super.onBrowserEvent(context, parent, value, event, valueUpdater);
38
39 if ("click".equals(event.getType())) {
40 onEnterKeyDown(context, parent, value, event, valueUpdater);
41 }
42
43 }
44
45 @Override
46 protected void onEnterKeyDown(
47 com.google.gwt.cell.client.Cell.Context context, Element parent,
48 ContentServerItem value, NativeEvent event,
49 ValueUpdater<ContentServerItem> valueUpdater) {
50
51 if (updater != null) {
52 updater.update(value);
53 }
54 }
55
56
57 /**
58 * Set the {@link ValueUpdater}.
59 *
60 * @param updater
61 * the value updater to use
62 */
63 public void setUpdater(ValueUpdater<ContentServerItem> updater) {
64 this.updater = updater;
65 }
66
67 public ContentServerItem fireInternalEvent(ContentServerItem value) {
68 return value;
69 }
70
71 @Override
72 protected void render(com.google.gwt.cell.client.Cell.Context context,
73 SafeHtml data, SafeHtmlBuilder sb) {
74
75 if (data != null) {
76 sb.append(data);
77 }
78
79 }
80 }

There is not really something special here inside. As I sad some of the functions are changed in GWT 2.2 so you do have to be careful. I learned how to extend the class also from the Expense example, actually the examples there are helping a lot. Basically most of the GWT cells are extending AbstractCell but the AbstractSafeHtmlCell has a render which allow us to use SafeHtml, that’s why I use it.


Now if you want to use your cell just initialize it


FolderItemClickableTextCell nameCell = new FolderItemClickableTextCell();


and put it into your table


cellTable.addColumn(…);


So I hope this example helps you somehow.

8 comments:

  1. very nice one! thank you.

    ReplyDelete
  2. How to do you go about when it comes to a composite cell which is consisted from 3-4 cells and each cell when clicked performs a different action?

    ReplyDelete
  3. hmm, interesting one, I have to check this first, I will let you know during the week.

    ReplyDelete
  4. Hi Lyudmil,
    Very nice example. Did you try looking into a cell with more than one image and each image being clickable?
    I have a celltable where I would like to sort the rows by clicking either a up or a down arrow. The two sort-direction arrows is placed in a cell on each row.
    DO you think that's possible?

    ReplyDelete
  5. Thanks, I wanted to try it this week, but due to lack of time I had to shift it for next week sorry about that. I will post here a example as soon as I tried it. I think what you want to do is possible, since you can control the cell render, you will just need a flag which shows which sorting you do and which arrow to show.

    ReplyDelete
  6. Take a look at the new example here:
    http://webcentersuite.blogspot.com/2011/11/custom-gwt-clickable-cell-with-multiple.html

    ReplyDelete
  7. Thank you so much. But got a question by the last step.
    How can you add a cell to a celltable by using celltable.addColumn(cell)?
    It is not working in my project. I really need to know how to do this.
    I apologize if this is a stupid question.

    ReplyDelete
  8. Take a look at the source code here:
    http://code.google.com/p/gwt2go/

    I think it will be the best way.

    ReplyDelete