001    /*
002     * Copyright 2006 Mat Gessel <mat.gessel@gmail.com>
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package asquare.gwt.tk.client.ui;
017    
018    import java.util.Vector;
019    
020    import asquare.gwt.tk.client.util.GwtUtil;
021    
022    import com.google.gwt.user.client.DOM;
023    import com.google.gwt.user.client.Element;
024    import com.google.gwt.user.client.ui.CellPanel;
025    import com.google.gwt.user.client.ui.HasAlignment;
026    import com.google.gwt.user.client.ui.Widget;
027    
028    /**
029     * A table-based panel which {@link #getCellElement(int) exposes} the TD element
030     * and supports:
031     * <ul>
032     * <li>setting styles on the TD</li>
033     * <li>multiple widgets per cell</li>
034     * <li>empty cells</li>
035     * </ul>
036     */
037    public abstract class ExposedCellPanel extends CellPanel implements HasAlignment
038    {
039            /**
040             * Maps the index of each cell to an ordered list of child widgets. 
041             * Mapping may be null if the cell contains no widgets. <br/>
042             */
043            private final Vector m_cellMap = new Vector();
044            
045            private HorizontalAlignmentConstant m_horizontalAlignment = ALIGN_LEFT;
046            private VerticalAlignmentConstant m_verticalAlignment = ALIGN_TOP;
047            
048            /**
049             * Creates an empty panel with no cells. 
050             */
051             ExposedCellPanel()
052            {
053                    DOM.setAttribute(getTable(), "cellSpacing", "0");
054                DOM.setAttribute(getTable(), "cellPadding", "0");
055            }
056            
057            /**
058             * Gets the number of cells in this panel. 
059             */
060            public int getCellCount()
061            {
062                    return m_cellMap.size();
063            }
064            
065            /**
066             * Adds a new cell to the panel. 
067             */
068            public void addCell()
069            {
070                    insertCell(m_cellMap.size());
071            }
072            
073            /**
074             * Creates a new cell and inserts it at the specified index. Existing cells
075             * with an index >= <code>cellIndex</code> will be shifted over by 1.
076             * 
077             * @param cellIndex the index where the cell will be inserted
078             * @throws IndexOutOfBoundsException if <code>cellIndex</code> is less
079             *             than 0 or greater than the number of cells
080             */
081            public void insertCell(int cellIndex)
082            {
083                    GwtUtil.rangeCheck(m_cellMap, cellIndex, true);
084                    
085                    m_cellMap.add(cellIndex, null);
086                    insertCellStructure(cellIndex);
087                    if (m_horizontalAlignment != null)
088                    {
089                            setCellHorizontalAlignment(cellIndex, m_horizontalAlignment);
090                    }
091                    if (m_verticalAlignment != null)
092                    {
093                            setCellVerticalAlignment(cellIndex, m_verticalAlignment);
094                    }
095            }
096            
097            /**
098             * A <em>template method</em> which creates a td and inserts it into the
099             * underlying table. If a cell exists at <code>cellIndex</code> it will be
100             * shifted up by 1. Implementors may assume <code>cellIndex</code> is
101             * greater than 0 and less than or equal to the number of cells.
102             * 
103             * @param cellIndex the index at which the td will be inserted.
104             */
105            protected abstract void insertCellStructure(int cellIndex);
106            
107            /**
108             * Removes a cell from the panel, including any child widets.
109             * 
110             * @param cellIndex the index of the cell
111             * @throws IndexOutOfBoundsException if the cell specified by
112             *             <code>cellIndex</code> does not exist
113             */
114            public void removeCell(int cellIndex)
115            {
116                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
117                    
118                    clearCell(cellIndex);
119                    removeCellStructure(cellIndex);
120                    m_cellMap.remove(cellIndex);
121            }
122            
123            /**
124             * Removes all widgets and child elements from the cell
125             * 
126             * @param cellIndex the index of the cell
127             * @throws IndexOutOfBoundsException if cellIndex does not specify a valid
128             *             cell
129             */
130            public void clearCell(int cellIndex)
131            {
132                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
133                    
134                    Vector cellWidgets = (Vector) m_cellMap.get(cellIndex);
135                    if (cellWidgets != null)
136                    {
137                            while (! cellWidgets.isEmpty())
138                            {
139                                    removeWidget((Widget) cellWidgets.lastElement(), cellIndex);
140                            }
141                    }
142                    DOM.setInnerHTML(getCellElement(cellIndex), "");
143                    m_cellMap.set(cellIndex, null);
144            }
145            
146            /**
147             * A <em>template method</em> which removes a td from the underlying
148             * table. Cells with indexes greater than <code>cellIndex</code> will be
149             * shifted down by 1. Implementors may assume <code>cellIndex</code> is
150             * greater than 0 and less than the number of cells.
151             * 
152             * @param cellIndex the index at which the td will be inserted.
153             */
154            protected abstract void removeCellStructure(int cellIndex);
155            
156            /**
157             * Removes all cells and child widgets from the panel. 
158             */
159            public void clear()
160            {
161                    while(! m_cellMap.isEmpty())
162                    {
163                            removeCell(m_cellMap.size() - 1);
164                    }
165            }
166            
167            /**
168             * Gets the number of child widgets added to the panel
169             * 
170             * @return the number of child widgets
171             */
172            public int getWidgetCount()
173            {
174                    return getChildren().size();
175            }
176            
177            /**
178             * Gets a at the specified index in the specified cell. <code>wIndex</code>
179             * does not include non-widget child elements.
180             * 
181             * @param cellIndex the index of the cell, starting with 0
182             * @param wIndex the index of the widget in the specified cell, starting at
183             *            0
184             * @return the widget
185             * @throws IndexOutOfBoundsException if the cell specified by
186             *             <code>cellIndex</code> does not exist
187             * @throws IndexOutOfBoundsException if the widget specified by
188             *             <code>wIndex</code> does not exist
189             */
190            public Widget getWidgetAt(int cellIndex, int wIndex)
191            {
192                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
193                    
194                    Vector childWidgets = (Vector) m_cellMap.get(cellIndex);
195                    
196                    if (childWidgets == null)
197                            throw new IndexOutOfBoundsException(Integer.toString(wIndex));
198                    
199                    GwtUtil.rangeCheck(childWidgets, wIndex, false);
200                    
201                    return (Widget) childWidgets.get(wIndex);
202            }
203            
204            /**
205             * Get the index of the cell which contains the specified widget.
206             * 
207             * @param w a child widget
208             * @return the index or <code>-1</code> if the widget is not a child of
209             *         this panel
210             */
211            public int getCellIndexOf(Widget w)
212            {
213                    int result = -1;
214                    int cell = 0, size = m_cellMap.size();
215                    while (cell < size)
216                    {
217                            Vector cellWidgets = (Vector) m_cellMap.get(cell);
218                            if (cellWidgets != null && cellWidgets.contains(w))
219                            {
220                                    result = cell;
221                                    break;
222                            }
223                            cell++;
224                    }
225                    return result;
226            }
227            
228            /**
229             * Creates a new cell and appends to it the specified widget. 
230             * 
231             * @param w a widget
232             */
233            public void add(Widget w)
234            {
235                    addWidget(w, true);
236            }
237            
238            /**
239             * Adds a widget to the panel, optionally creating a new cell. 
240             * 
241             * @param w a widget
242             * @param newCell <code>true</code> to create a new cell,
243             *            <code>false</code> to append to the last cell
244             * @throws IndexOutOfBoundsException if <code>newCell</code> is
245             *             <code>false</code> and no cells exist
246             */
247            public void addWidget(Widget w, boolean newCell)
248            {
249                    /*
250                     * if w is a child of this panel and the only widget in a cell the cell
251                     * will be removed
252                     */
253                    w.removeFromParent();
254                    
255                    if (newCell)
256                    {
257                            insertCell(m_cellMap.size());
258                    }
259                    addWidgetTo(w, m_cellMap.size() - 1);
260            }
261            
262            /**
263             * Adds a widget to the specified cell. <code>w</code> will be appended
264             * after any other widgets in the cell.
265             * 
266             * @param w a widget
267             * @param cellIndex the index of the cell
268             * @throws IndexOutOfBoundsException if the cell specified by
269             *             <code>cellIndex</code> does not exist
270             */
271            public void addWidgetTo(Widget w, int cellIndex)
272            {
273                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
274                    
275                    Vector cellWidgets = (Vector) m_cellMap.get(cellIndex);
276                    int wIndex = (cellWidgets != null) ? cellWidgets.size() : 0;
277                    insertWidgetAt(w, cellIndex, wIndex);
278            }
279            
280            /**
281             * Inserts a new cell at the specified index and appends the widget to the
282             * cell.
283             * 
284             * @param w a widget
285             * @param cellIndex the index of the cell
286             * @throws IndexOutOfBoundsException if <code>cellIndex</code> is less
287             *             than 0 or greater than the number of cells
288             */
289            public void insert(Widget w, int cellIndex)
290            {
291                    /*
292                     * if w is a child of this panel and the only widget in a cell the cell
293                     * will be removed
294                     */
295                    w.removeFromParent();
296                    
297                    insertCell(cellIndex);
298                    insertWidgetAt(w, cellIndex, 0);
299            }
300            
301            /**
302             * Inserts a widget into a an existing cell. Any widgets with indexes
303             * greater than or equal to <code>wIndex</code> will be shifted over.
304             * 
305             * @param w a widget
306             * @param cellIndex the index of the cell
307             * @param wIndex the index of the widget before which <code>w</code> will
308             *            be inserted
309             * @throws IndexOutOfBoundsException if the cell specified by
310             *             <code>cellIndex</code> does not exist
311             * @throws IndexOutOfBoundsException if <code>wIndex</code> is less than 0
312             *             or greater than the number of widgets in the specified cell
313             */
314            public void insertWidgetAt(Widget w, int cellIndex, int wIndex)
315            {
316                    /*
317                     * The cell will be removed if w is a child of this panel and w is the
318                     * only widget in the cell
319                     */
320                    w.removeFromParent();
321                    
322                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
323                    
324                    Vector cellWidgets = (Vector) m_cellMap.get(cellIndex);
325                    
326                    if (cellWidgets == null)
327                    {
328                            if (wIndex != 0)
329                                    throw new IndexOutOfBoundsException(Integer.toString(wIndex));
330    
331                            cellWidgets = new Vector();
332                            m_cellMap.set(cellIndex, cellWidgets); // pre: cellIndex has been inserted
333                    }
334                    
335                    GwtUtil.rangeCheck(cellWidgets, wIndex, true);
336                    
337                    cellWidgets.insertElementAt(w, wIndex);
338                    
339                    /*
340                     * Pass null and append the element manually so that we can specify
341                     * insertion order. Also note that the order of the "children"
342                     * WidgetCollection is not maintained.
343                     */
344                    insert(w, null, getChildren().size());
345                    Element td = getCellElement(cellIndex);
346                    DOM.insertChild(td, w.getElement(), wIndex);
347            }
348            
349            /**
350             * Removes a widget from the panel. 
351             * Automatically removes the cell if it becomes empty. 
352             * 
353             * @param w a child widget
354             * @return false if <code>w</code> is not a child of this panel
355             */
356            public boolean remove(Widget w)
357            {
358                    return remove(w, true);
359            }
360            
361            /**
362             * Removes a widget from the panel, optionally removing the cell if it
363             * becomes empty.
364             * 
365             * @param w a child widget
366             * @param removeEmptyCell <code>true</code> to remove the cell if it
367             *            becomes empty
368             * @return false if <code>w</code> is not a child of this panel
369             */
370            public boolean remove(Widget w, boolean removeEmptyCell)
371            {
372                    if (! getChildren().contains(w))
373                            return false;
374                    
375                    int cellIndex = getCellIndexOf(w);
376                    assert cellIndex >= 0;
377                    
378                    removeWidget(w, cellIndex);
379                    Vector childWidgets = (Vector) m_cellMap.get(cellIndex);
380                    if (removeEmptyCell && childWidgets.size() == 0)
381                    {
382                            removeCell(cellIndex);
383                    }
384                    return true;
385            }
386            
387            /*
388             * Removes the widget from the cellMap & DOM tree and clears out its
389             * listener
390             */
391            private void removeWidget(Widget w, int cellIndex)
392            {
393                    Vector childWidgets = (Vector) m_cellMap.get(cellIndex);
394                    
395                    super.remove(w);
396                    boolean present = (childWidgets).remove(w);
397                    assert present;
398            }
399            
400            /**
401             * Gets the table <code>td</code> element corresponding to the specified
402             * cell.
403             * 
404             * @param cellIndex the index of the cell
405             * @return the <code>td</code> element of the specified cell
406             * @throws IndexOutOfBoundsException if <code>cellIndex</code> does not
407             *             specify an existing cell
408             */
409            public abstract Element getCellElement(int cellIndex);
410            
411            /**
412             * Gets the style name(s) for the cell specified by
413             * <code>cellIndex</code>.
414             * 
415             * @return the CSS class name(s) (space delimited)
416             * @param cellIndex the index of the cell
417             * @throws IndexOutOfBoundsException if the cell specified by
418             *             <code>cellIndex</code> does not exist
419             */
420            public String getCellStyleName(int cellIndex)
421            {
422                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
423                    
424                    return DOM.getAttribute(getCellElement(cellIndex), "className");
425            }
426            
427            /**
428             * Adds a style name to the last cell. 
429             * 
430             * @param styleName a CSS class name
431             * @throws IllegalStateException if no cells exist
432             * @throws IllegalArgumentException if <code>styleName</code> is <code>&quot;&quot;</code>
433             */
434            public void addCellStyleName(String styleName)
435            {
436                    if (getCellCount() == 0)
437                            throw new IllegalStateException();
438                    
439                    addCellStyleName(getCellCount() - 1, styleName);
440            }
441            
442            /**
443             * Adds a style name to the <code>class</code> attribute of the cell
444             * specified by the <code>cellIndex</code>.
445             * 
446             * @param cellIndex the index of a cell
447             * @param styleName a CSS class name
448             * @throws IndexOutOfBoundsException if the cell specified by
449             *             <code>cellIndex</code> does not exist
450             * @throws IllegalArgumentException if <code>styleName</code> is <code>&quot;&quot;</code>
451             */
452            public void addCellStyleName(int cellIndex, String styleName)
453            {
454                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
455                    
456                    setStyleName(getCellElement(cellIndex), styleName, true);
457            }
458            
459            /**
460             * Sets the style name for the last cell. Other style names will be
461             * overwritten.
462             * 
463             * @param styleName a CSS class name
464             * @throws IllegalStateException if no cells exist
465             */
466            public void setCellStyleName(String styleName)
467            {
468                    if (m_cellMap.size() == 0)
469                            throw new IllegalStateException();
470                    
471                    setCellStyleName(getCellCount() - 1, styleName);
472            }
473            
474            /**
475             * Sets the style name for the cell specified by
476             * <code>cellIndex</code>. Other style names will be
477             * overwritten.
478             * 
479             * @param cellIndex the index of the cell
480             * @param styleName a CSS class name
481             * @throws IndexOutOfBoundsException if the cell specified by
482             *             <code>cellIndex</code> does not exist
483             */
484            public void setCellStyleName(int cellIndex, String styleName)
485            {
486                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
487                    
488                    DOM.setAttribute(getCellElement(cellIndex), "className", styleName);
489            }
490            
491            /**
492             * Removes the specified style name from the last cell. Does nothing if
493             * <code>styleName</code> is not present.
494             * 
495             * @param styleName a CSS class name
496             * @throws IllegalStateException if no cells exist
497             * @throws IllegalArgumentException if <code>styleName</code> is
498             *             <code>&quot;&quot;</code>
499             */
500            public void removeCellStyleName(String styleName)
501            {
502                    if (getCellCount() == 0)
503                            throw new IllegalStateException();
504                    
505                    removeCellStyleName(getCellCount() - 1, styleName);
506            }
507            
508            /**
509             * Adds a style name to the cell specified by
510             * <code><cellIndex/code>. Does nothing if
511             * <code>styleName</code> is not present.
512             * 
513             * @param cellIndex the index of the cell
514             * @param styleName a CSS class name
515             * @throws IndexOutOfBoundsException if the cell specified by
516             *             <code>cellIndex</code> does not exist
517             * @throws IllegalArgumentException if <code>styleName</code> is <code>&quot;&quot;</code>
518             */
519            public void removeCellStyleName(int cellIndex, String styleName)
520            {
521                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
522                    
523                    setStyleName(getCellElement(cellIndex), styleName, false);
524            }
525            
526            /**
527             * Sets the width of the last cell. 
528             * 
529             * @param width a CSS measurement
530             */
531            public void setCellWidth(String width)
532            {
533                    if (m_cellMap.size() == 0)
534                            throw new IllegalStateException();
535                    
536                    setCellWidth(getCellCount() - 1, width);
537            }
538            
539            /**
540             * Sets the width of the specified cell. 
541             * 
542             * @param cellIndex the index of a cell
543             * @param width a CSS measurement
544             * @throws IndexOutOfBoundsException if the cell specified by
545             *             <code>cellIndex</code> does not exist
546             */
547            public void setCellWidth(int cellIndex, String width)
548            {
549                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
550                    
551                    DOM.setAttribute(getCellElement(cellIndex), "width", width);
552            }
553    
554            /**
555             * Sets the height of the last cell. 
556             * 
557             * @param height a CSS measurement
558             */
559            public void setCellHeight(String height)
560            {
561                    if (m_cellMap.size() == 0)
562                            throw new IllegalStateException();
563                    
564                    setCellHeight(getCellCount() - 1, height);
565            }
566            
567            /**
568             * Sets the height of the specified cell. 
569             * 
570             * @param cellIndex the index of a cell
571             * @param height a CSS measurement
572             * @throws IndexOutOfBoundsException if the cell specified by
573             *             <code>cellIndex</code> does not exist
574             */
575            public void setCellHeight(int cellIndex, String height)
576            {
577                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
578                    
579                    DOM.setAttribute(getCellElement(cellIndex), "height", height);
580            }
581            
582            /**
583             * Sets the horizontal alignment of the last cell. 
584             * 
585             * @param hAlign a constant representing left, center or right alignment
586             * @see HasAlignment
587             */
588            public void setCellHorizontalAlignment(HorizontalAlignmentConstant hAlign)
589            {
590                    if (m_cellMap.size() == 0)
591                            throw new IllegalStateException();
592                    
593                    setCellHorizontalAlignment(getCellCount() - 1, hAlign);
594            }
595            
596            /**
597             * Sets the horizontal alignment of the specified cell. 
598             * 
599             * @param cellIndex the index of a cell
600             * @param hAlign a constant representing left, center or right alignment
601             * @throws IndexOutOfBoundsException if the cell specified by
602             *             <code>cellIndex</code> does not exist
603             * @see HasAlignment
604             */
605            public void setCellHorizontalAlignment(int cellIndex, HorizontalAlignmentConstant hAlign)
606            {
607                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
608                    
609                    DOM.setAttribute(getCellElement(cellIndex), "align", hAlign.getTextAlignString());
610            }
611            
612            /**
613             * Sets the vertical alignment of the last cell. 
614             * 
615             * @param vAlign a constant representing top, middle or bottom alignment
616             * @see HasAlignment
617             */
618            public void setCellVerticalAlignment(VerticalAlignmentConstant vAlign)
619            {
620                    if (m_cellMap.size() == 0)
621                            throw new IllegalStateException();
622                    
623                    setCellVerticalAlignment(getCellCount() - 1, vAlign);
624            }
625            
626            /**
627             * Sets the vertical alignment of the specified cell. 
628             * 
629             * @param cellIndex the index of a cell
630             * @param vAlign a constant representing top, middle or bottom alignment
631             * @throws IndexOutOfBoundsException if the cell specified by
632             *             <code>cellIndex</code> does not exist
633             * @see HasAlignment
634             */
635            public void setCellVerticalAlignment(int cellIndex, VerticalAlignmentConstant vAlign)
636            {
637                    GwtUtil.rangeCheck(m_cellMap, cellIndex, false);
638                    
639                    DOM.setStyleAttribute(getCellElement(cellIndex), "verticalAlign", vAlign.getVerticalAlignString());
640            }
641            
642            /**
643             * Gets the default horizontal alignment for newly created cells.
644             * 
645             * @return an alignment constant or null
646             * @see HasAlignment
647             */
648            public HorizontalAlignmentConstant getHorizontalAlignment()
649            {
650                    return m_horizontalAlignment;
651            }
652            
653            /**
654             * Sets the default horizontal alignment for newly created cells.
655             * 
656             * @param align an alignment constant or null
657             * @see HasAlignment
658             */
659            public void setHorizontalAlignment(HorizontalAlignmentConstant align)
660            {
661                    m_horizontalAlignment = align;
662            }
663            
664            /**
665             * Gets the default vertical alignment for newly created cells.
666             * 
667             * @return an alignment constant or null
668             * @see HasAlignment
669             */
670            public VerticalAlignmentConstant getVerticalAlignment()
671            {
672                    return m_verticalAlignment;
673            }
674            
675            /**
676             * Sets the default vertical alignment for newly created cells.
677             * 
678             * @param align an alignment constant or null
679             * @see HasAlignment
680             */
681            public void setVerticalAlignment(VerticalAlignmentConstant align)
682            {
683                    m_verticalAlignment = align;
684            }
685    }