001: package net.xoetrope.awt;
002:
003: import java.awt.AWTEventMulticaster;
004: import java.awt.Canvas;
005: import java.awt.Color;
006: import java.awt.Component;
007: import java.awt.Container;
008: import java.awt.Dimension;
009: import java.awt.Font;
010: import java.awt.FontMetrics;
011: import java.awt.Graphics;
012: import java.awt.ItemSelectable;
013: import java.awt.ScrollPane;
014: import java.awt.event.ItemEvent;
015: import java.awt.event.ItemListener;
016: import java.awt.event.KeyEvent;
017: import java.awt.event.KeyListener;
018: import java.awt.event.MouseEvent;
019: import java.awt.event.MouseListener;
020:
021: import net.xoetrope.xui.XPage;
022: import net.xoetrope.xui.XProjectManager;
023: import net.xoetrope.xui.data.XDataBinding;
024: import net.xoetrope.xui.data.XModel;
025: import net.xoetrope.xui.data.XTextBinding;
026: import net.xoetrope.xui.style.XStyle;
027: import java.awt.Rectangle;
028:
029: /**
030: * <p>Provides a simple read-only tables/grid component.</p>
031: * <p>Copyright (c) Xoetrope Ltd., 1998-2004<br>
032: * License: see license.txt
033: * $Revision: 1.27 $
034: */
035: public class XTableRenderer extends Canvas implements MouseListener,
036: KeyListener, ItemSelectable {
037: private int[] colWidth;
038: private int colPadding = 2;
039: private int currentY = 0;
040: private int itemIdx;
041: private XModel model;
042: private Font font;
043: private FontMetrics fontMetrics;
044: private int fontHeight;
045: private int rowHeight;
046: private int headerHeight;
047: private String tableStyle, headerStyle, selectedStyle;
048: private Color backColor, foreColor, darkerColor;
049: private static final double darkerFactor = 0.95;
050: private boolean updateModelSelection;
051:
052: private boolean interactiveTable;
053: private boolean drawFrame;
054: private int selectedRow;
055: private int selectedColumn;
056: private int startRow;
057: private XTable owner;
058: private boolean drawBorder;
059: private Color borderColor;
060:
061: private static final int VK_KP_UP = 0xE0;
062: private static final int VK_KP_DOWN = 0xE1;
063: private static final int VK_KP_LEFT = 0xE2;
064: private static final int VK_KP_RIGHT = 0xE3;
065: private Object[] components;
066: private Component currentComponent = null;
067: private XDataBinding editBinding;
068: private boolean rendered = false;
069:
070: public XTableRenderer(XTable parent) {
071: interactiveTable = false;
072: drawFrame = false;
073: selectedRow = 0;
074: selectedColumn = 0;
075: startRow = 0;
076: fontHeight = 13;
077:
078: addMouseListener(this );
079: addKeyListener(this );
080:
081: owner = parent;
082: }
083:
084: /**
085: * Set the XModel which we will be generating the table from
086: * @param xmodel The XModel of data
087: */
088: public void setContent(XModel xmodel) {
089: model = xmodel;
090: if (model != null) {
091: int numChildren = model.get(0).getNumChildren();
092: components = new Object[numChildren];
093: colWidth = new int[numChildren];
094: for (int i = 0; i < colWidth.length; i++)
095: colWidth[i] = 100;
096: } else
097: components = null;
098: }
099:
100: /**
101: * Set the type of component for a column
102: * @param col the field index
103: * @param className the class name
104: */
105: public void setComponentAt(int col, String compName) {
106: components[col] = compName;
107: }
108:
109: /**
110: * Set the general style of the XTable
111: * @param style XStyle
112: */
113: public void setStyle(String style) {
114: if (style == null)
115: tableStyle = "base";
116: else
117: tableStyle = style;
118: }
119:
120: /**
121: * Set the style of the header data
122: * @param style XStyle
123: */
124: public void setHeaderStyle(String style) {
125: headerStyle = style;
126: }
127:
128: /**
129: * Set the style of the selected row
130: * @param style XStyle
131: */
132: public void setSelectedStyle(String style) {
133: selectedStyle = style;
134: }
135:
136: /**
137: * Set teh style for the border
138: * @param styleName
139: */
140: public void setBorderStyle(String styleName) {
141: XStyle style = XProjectManager.getStyleManager().getStyle(
142: styleName);
143:
144: borderColor = style.getStyleAsColor(XStyle.COLOR_FORE);
145: drawBorder = true;
146: }
147:
148: /**
149: * Check the if the table is interactive
150: * @return true if the table supports user interaction
151: */
152: public boolean isInteractiveTable() {
153: return interactiveTable;
154: }
155:
156: /**
157: * Set the user interaction state
158: * @param state true for an user interactive table.
159: */
160: public void setInteractiveTable(boolean state) {
161: interactiveTable = state;
162: repaint();
163: }
164:
165: /**
166: * Draw an individual row of data. Alternate the backcolor for every other
167: * row. Set the clip region and fill the background. Loop each XModel in
168: * the model parameter drawing the values and drawing a vertical line to the
169: * right of the cell. Draw a line under the row
170: * @param row The index of the row being rendered
171: * @param g The Graphics object
172: * @param model The XModel containing the row of data
173: */
174: private void renderRow(int row, Graphics g, XModel model,
175: boolean headerCell) {
176: int currentX = drawFrame ? 1 : 0;
177: clearRow(g, row);
178:
179: for (int fieldIdx = 0; fieldIdx < model.getNumChildren(); fieldIdx++) {
180: if (model.get(fieldIdx) != null) {
181: try {
182: currentX = renderCell(g, currentX, model
183: .get(fieldIdx), (String) model
184: .get(fieldIdx).get(), fieldIdx, headerCell);
185: } catch (Exception ex) {
186: ex.printStackTrace();
187: }
188: }
189: }
190: }
191:
192: /**
193: * Render a table cell
194: * @param g The graphics context
195: * @param currentX the X position
196: * @param value the value to render
197: * @param fieldIdx the field index
198: * @param headerCell true if a header cell is being rendered
199: * @return the new X position
200: */
201: private int renderCell(Graphics g, int currentX, XModel model,
202: String value, int fieldIdx, boolean headerCell) {
203: Rectangle clipRect = g.getClipBounds();
204: int clipY = currentY - fontMetrics.getAscent() - colPadding;
205: int clipW = clipRect.width - (currentX - clipRect.x);
206: int clipH = Math.min(clipRect.height - (clipY - clipRect.y),
207: getSize().height);
208: g.setColor(foreColor);
209: g.setClip(currentX, clipY, Math.min(colWidth[fieldIdx]
210: - (colPadding * 2), clipW), clipH);
211: if (value != null)
212: g.drawString(value, currentX + colPadding, currentY);
213:
214: g.setColor(borderColor);
215: g.setClip(currentX, clipY, Math.min(getSize().width, clipW),
216: clipH);
217:
218: if (drawBorder)
219: g.drawRect(currentX, currentY - fontMetrics.getAscent()
220: - colPadding, colWidth[fieldIdx] - 1, fontHeight
221: + (colPadding * 2));
222:
223: // Draw a vertical line to the right of the cell
224: g.drawLine(currentX + colWidth[fieldIdx] - 1, currentY
225: - fontMetrics.getAscent() - colPadding, currentX
226: + colWidth[fieldIdx] - 1, currentY + (colPadding * 2)
227: + 1);
228: currentX += colWidth[fieldIdx];
229: g.setClip(clipRect);
230:
231: return currentX;
232: }
233:
234: /**
235: * Erase the backgroudn of a row
236: * @param g The graphics context
237: * @param row the row number
238: */
239: private void clearRow(Graphics g, int row) {
240: if (row % 2 == 1)
241: g.setColor(darkerColor);
242: else
243: g.setColor(backColor);
244:
245: int offset = drawFrame ? 1 : 0;
246: g.setClip(offset, offset, getSize().width - 1 - offset,
247: getSize().height);
248: g.fillRect(offset, currentY - fontMetrics.getAscent()
249: - colPadding, getSize().width, fontHeight
250: + (colPadding * 2));
251: }
252:
253: /**
254: * Get the parent element from the XModel. Apply the header style increment
255: * the currentY and draw the first row of data in the XModel. Apply the general
256: * style, loop thru the remaining elements in the XModel and render them.
257: * @param g The graphics object
258: */
259: private void render(Graphics g) {
260: rendered = true;
261: if (model != null) {
262: itemIdx = 0;
263: int startClipY = (int) g.getClipBounds().y;
264: int endClipY = (int) (g.getClipBounds().height + g
265: .getClipBounds().y);
266:
267: // Render the header
268: startRow = renderHeader(g, model, startClipY);
269: headerHeight = currentY + (colPadding * 2) + 1;
270:
271: // Render the content
272: int numChildren = model.getNumChildren();
273: applyStyle(g, tableStyle);
274: if (borderColor == null)
275: borderColor = getForeground().darker();
276:
277: int currentRow = getCurrentRow();
278: for (int rowIdx = startRow; rowIdx < numChildren; rowIdx++) {
279: if (interactiveTable && (rowIdx == currentRow))
280: applySelectedStyle(g, selectedStyle);
281:
282: currentY += fontHeight + (colPadding * 2);
283: XModel rowModel = model.get(rowIdx);
284: if (rowModel != null) {
285: if (((String) rowModel.get(0).get()).length() > 0)
286: itemIdx++;
287:
288: if (currentY > (startClipY - (fontHeight + (colPadding * 2))))
289: renderRow(itemIdx, g, rowModel, false);
290:
291: // Reapply the normal style
292: if (interactiveTable && (rowIdx == currentRow))
293: applyStyle(g, tableStyle);
294: endClipY = (int) (g.getClipBounds().height + g
295: .getClipBounds().y);
296: if (currentY > endClipY)
297: break;
298: }
299: }
300:
301: if ((currentRow >= 0) && (currentRow < numChildren))
302: model.get(currentRow);
303: }
304: }
305:
306: /**
307: * Renders the table header
308: * @param g The graphics object
309: * @param model The data model
310: */
311: private int renderHeader(Graphics g, XModel model, int startClipY) {
312: applyStyle(g, headerStyle);
313:
314: if (model.getNumChildren() > 0) {
315: XModel rowModel = model.get(0);
316: String tag = rowModel.getTagName();
317: if (tag.equalsIgnoreCase("th")) {
318: // A <th>...</th> header record
319: currentY += fontMetrics.getAscent() + colPadding;
320: if (startClipY < rowHeight)
321: renderRow(0, g, rowModel, true);
322: return 1;
323: } else if (tag.equalsIgnoreCase("tr")) {
324: // No header specified in the XML
325: return 0;
326: } else {
327: // An extended model node type e.g. XLib::XTableModelNode
328: currentY += fontMetrics.getAscent() + colPadding;
329: if (startClipY >= rowHeight)
330: return 0;
331:
332: int currentX = 0;
333: clearRow(g, 0);
334:
335: for (int fieldIdx = 0; fieldIdx < model
336: .getNumAttributes(); fieldIdx++)
337: currentX = renderCell(g, currentX, model, model
338: .getAttribName(fieldIdx), fieldIdx, true);
339:
340: // Draw a line under the row
341: g.drawLine(1, currentY + (colPadding * 2) + 1,
342: getSize().width, currentY + (colPadding * 2)
343: + 1);
344: }
345: }
346:
347: return 0;
348: }
349:
350: /**
351: * Initialise the currentY coordinate and call the render function with the
352: * Graphics object. When finished draw a line around the XTable.
353: * @param g
354: */
355: public void paint(Graphics g) {
356: update(g);
357: }
358:
359: /**
360: * Applies a named style to the Graphics context.
361: * @param styleName
362: * @param g
363: */
364: private void applyStyle(Graphics g, String styleName) {
365: if (styleName == null)
366: styleName = "base";
367:
368: XStyle style = XProjectManager.getStyleManager().getStyle(
369: styleName);
370:
371: foreColor = style.getStyleAsColor(XStyle.COLOR_FORE);
372: if (foreColor == null)
373: foreColor = getForeground();
374:
375: backColor = style.getStyleAsColor(XStyle.COLOR_BACK);
376: if (backColor == null)
377: backColor = getBackground();
378:
379: if (backColor != null) {
380: darkerColor = new Color(
381: (int) (backColor.getRed() * darkerFactor),
382: (int) (backColor.getGreen() * darkerFactor),
383: (int) (backColor.getBlue() * darkerFactor));
384: }
385:
386: font = XProjectManager.getStyleManager().getFont(style);
387: g.setFont(font);
388: fontMetrics = g.getFontMetrics();
389: fontHeight = fontMetrics.getHeight();
390: rowHeight = (fontHeight + (colPadding * 2));
391: }
392:
393: /**
394: * Applies a named style to the Graphics context.
395: * @param styleName
396: * @param g
397: */
398: private void applySelectedStyle(Graphics g, String styleName) {
399: if (styleName == null)
400: styleName = "base";
401:
402: XStyle style = XProjectManager.getStyleManager().getStyle(
403: styleName);
404:
405: foreColor = style.getStyleAsColor(XStyle.COLOR_FORE);
406: if (foreColor == null)
407: foreColor = getForeground();
408:
409: backColor = style.getStyleAsColor(XStyle.COLOR_BACK);
410: if (backColor == null)
411: backColor = getBackground();
412: darkerColor = backColor;
413: }
414:
415: /**
416: * Sets the indexof the selected row
417: * @param idx the new selected row
418: */
419: public void setSelectedRow(int idx) {
420: selectedRow = Math.max(0, Math.min(idx,
421: model.getNumChildren() - 1));
422: syncModel();
423: repaint();
424: }
425:
426: /**
427: * Get the index of the selected row
428: * @return the index of the selected row
429: */
430: public int getSelectedRow() {
431: return selectedRow;
432: }
433:
434: /**
435: * Tie the model selection to this table's selection
436: * @param doUpdate true to tie the selections together, false to ignore
437: */
438: public void setUpdateModelSelection(boolean doUpdate) {
439: updateModelSelection = doUpdate;
440: syncModel();
441: }
442:
443: /**
444: * Update the underlying model's selection index with the tables index.
445: */
446: private void syncModel() {
447: if (updateModelSelection)
448: model.get(startRow + selectedRow);
449: }
450:
451: /**
452: * Handles the mouse click by changeing the selected row.
453: * @param e
454: */
455: public void mouseClicked(MouseEvent e) {
456: }
457:
458: public void mouseEntered(MouseEvent e) {
459: }
460:
461: public void mouseExited(MouseEvent e) {
462: }
463:
464: public void mousePressed(MouseEvent e) {
465: }
466:
467: public void mouseReleased(MouseEvent e) {
468: removeCurrentComponent();
469:
470: rowHeight = (fontHeight + (colPadding * 2));
471: int oldSelection = selectedRow;
472: int y = e.getY();
473: int x = e.getX();
474: y -= headerHeight;
475: if (model != null) {
476: int maxRow = model.getNumChildren() - 1;
477: selectedRow = Math.min(y / rowHeight, maxRow);
478:
479: int maxCol = components.length;
480: selectedColumn = 0;
481: int xMouse = x;
482: for (int i = 0; i < maxCol; i++) {
483: xMouse -= colWidth[i];
484: if (xMouse > 0)
485: selectedColumn++;
486: else
487: break;
488: }
489:
490: if (selectedColumn < components.length) {
491: if (components[selectedColumn] != null) {
492: setCellComponent(selectedColumn, selectedRow);
493: }
494: }
495:
496: repaint(0, headerHeight + rowHeight * oldSelection, 1000,
497: rowHeight);
498: repaint(0, headerHeight + rowHeight * selectedRow, 1000,
499: rowHeight);
500:
501: if (itemListener != null)
502: itemListener.itemStateChanged(new ItemEvent(owner,
503: ItemEvent.ITEM_STATE_CHANGED, new Integer(
504: selectedRow), ItemEvent.SELECTED));
505: }
506: syncModel();
507: }
508:
509: public void removeCurrentComponent() {
510: if (currentComponent != null)
511: owner.getComponentPanel().remove(currentComponent);
512: }
513:
514: /**
515: * Check to see if there is a value in the components array at position 'col'
516: * If so create an instance of the component and add it to the componentPanel
517: * Create a binding to the model and set it to the local variable 'editBinding'
518: * @param col The column of the model which was selected
519: * @param row The row of the model which was selected
520: */
521: private void setCellComponent(int col, int row) {
522: try {
523: int x = 0;
524: for (int i = 0; i < col; i++)
525: x += colWidth[i];
526: int width = colWidth[col] - colPadding;
527: int y = rowHeight + (rowHeight * row);
528: int height = rowHeight;
529: if ((row + 1) < model.getNumChildren()) {
530: Component c = (Component) Class.forName(
531: components[col].toString()).newInstance();
532: c.addKeyListener(this );
533: c.setBounds(x, y, width, height);
534: Container panel = owner.getComponentPanel();
535: panel.add(c, 0);
536: currentComponent = c;
537: XModel rowModel = (XModel) model.get(row + 1);
538: XModel bindModel = (XModel) rowModel.get(col);
539: XTextBinding binding = new XTextBinding(c, "", null);
540: binding.setSource(bindModel);
541: binding.setOutput(bindModel);
542: binding.get();
543: editBinding = binding;
544: }
545: } catch (Exception ex) {
546: ex.printStackTrace();
547: }
548: }
549:
550: public void keyPressed(KeyEvent e) {
551: if (!e.getSource().equals(currentComponent)) {
552: if (model != null) {
553: int oldSelection = selectedRow;
554: int numChildren = model.getNumChildren();
555: int keyCode = e.getKeyCode();
556: ScrollPane sp = ((ScrollPane) getParent().getParent());
557: int y = (int) sp.getScrollPosition().y;
558: if ((keyCode == e.VK_UP) || (keyCode == VK_KP_UP)) {
559: selectedRow = Math.max(0, selectedRow - 1);
560: sp.setScrollPosition(0, y - rowHeight);
561: } else if ((keyCode == e.VK_DOWN)
562: || (keyCode == VK_KP_DOWN)) {
563: selectedRow = Math.min(numChildren - 1,
564: selectedRow + 1);
565: sp.setScrollPosition(0, Math.min(y + rowHeight,
566: ((numChildren - 2) * rowHeight)));
567: } else
568: return;
569:
570: repaint(0, headerHeight + rowHeight * oldSelection,
571: 1000, rowHeight);
572: repaint(0, headerHeight + rowHeight * selectedRow,
573: 1000, rowHeight);
574:
575: if (itemListener != null)
576: itemListener.itemStateChanged(new ItemEvent(owner,
577: ItemEvent.ITEM_STATE_CHANGED, new Integer(
578: selectedRow), ItemEvent.SELECTED));
579: }
580: }
581: syncModel();
582: }
583:
584: public void keyTyped(KeyEvent e) {
585: }
586:
587: public void keyReleased(KeyEvent e) {
588: if (e.getSource().equals(currentComponent))
589: editBinding.set();
590: }
591:
592: public void update(Graphics g) {
593: if (drawFrame)
594: g.drawRect(0, 0, getSize().width - 1, getSize().height - 1);
595:
596: currentY = 0;
597: render(g);
598: // g.drawRect( 0, 0, getSize().width - 1, getSize().height - 1 );
599: }
600:
601: /**
602: * Calcualte the size of the content. This method is called from within the
603: * paint method and recalculates the required size for display of the content.
604: * If a scrollpane is the parent then this control is resized so that all the
605: * content will be visible. The scrollpane may initially have no scrollbar so
606: * to avoid flicker and multiple repaints as the control is sized and offscreen
607: * graphics context is used for the sizing.
608: */
609: public Dimension calcSize() {
610: Dimension d = new Dimension();
611:
612: if ((model != null) && (model.getNumChildren() > 0)) {
613: // This width should be calculated based on the content of the cells.
614: XModel rowModel = model.get(0);
615: int numCols = rowModel.getNumChildren();
616: for (int i = 0; i < numCols; i++)
617: d.width += colWidth[i];
618:
619: Graphics g = getGraphics();
620: if (g != null) {
621: if (tableStyle != null) {
622: // This doesn't properly account for different font sizes in the header but
623: // a little more or less height probably won't matter much.
624: XStyle style = XProjectManager.getStyleManager()
625: .getStyle(tableStyle);
626: font = XProjectManager.getStyleManager().getFont(
627: style);
628: }
629: g.setFont(font);
630: fontMetrics = g.getFontMetrics();
631: fontHeight = fontMetrics.getHeight();
632: g.dispose();
633: }
634:
635: d.height = ((fontHeight + (colPadding * 2) + 1))
636: * (model.getNumChildren() + 1) + 10;
637: }
638: return d;
639: }
640:
641: int getNumRows() {
642: if (model != null)
643: return model.getNumChildren();
644: return 0;
645: }
646:
647: /**
648: * Set the table column width.
649: * @param fieldIdx the field index
650: * @param w the new column width
651: */
652: public void setColWidth(int fieldIdx, int w) {
653: colWidth[fieldIdx] = w;
654: Dimension d = calcSize();
655: setBounds(0, 0, (int) d.getSize().width,
656: (int) d.getSize().height);
657: owner.getComponentPanel().setBounds(0, 0,
658: (int) d.getSize().width, (int) d.getSize().height);
659: }
660:
661: /**
662: * Gets the index in the model of the currently selected row.
663: * @return the row offset
664: */
665: public int getCurrentRow() {
666: return startRow + selectedRow;
667: }
668:
669: /**
670: * Gets the offset in the model of the first row of data. This takes account
671: * of how the table header is stored in the model. In the static data or XML
672: * representations the header is recorded with a row of <TH> elements, whereas
673: * when the data has originated in a database then a custom node type may be
674: * used instead.
675: * @return the row offset
676: */
677: public int getFirstRow() {
678: return startRow;
679: }
680:
681: /**
682: * Adds the specified item listener to receive item events from
683: * this list. Item events are sent in response to user input, but not
684: * in response to calls to <code>select</code> or <code>deselect</code>.
685: * If listener <code>l</code> is <code>null</code>,
686: * no exception is thrown and no action is performed.
687: *
688: * @param l the item listener
689: * @see #removeItemListener( ItemListener )
690: * @see java.awt.event.ItemEvent
691: * @see java.awt.event.ItemListener
692: * @since JDK1.1
693: */
694: public synchronized void addItemListener(ItemListener l) {
695: if (l == null) {
696: return;
697: }
698: itemListener = AWTEventMulticaster.add(itemListener, l);
699: }
700:
701: /**
702: * Removes the specified item listener so that it no longer
703: * receives item events from this list.
704: * If listener <code>l</code> is <code>null</code>,
705: * no exception is thrown and no action is performed.
706: *
707: * @param l the item listener
708: * @see #addItemListener
709: * @see java.awt.event.ItemEvent
710: * @see java.awt.event.ItemListener
711: * @since JDK1.1
712: */
713: public synchronized void removeItemListener(ItemListener l) {
714: if (l == null) {
715: return;
716: }
717: itemListener = AWTEventMulticaster.remove(itemListener, l);
718: }
719:
720: /**
721: * Returns the selected items on the list in an array of objects.
722: * @see ItemSelectable
723: */
724: public Object[] getSelectedObjects() {
725: Integer sel[] = new Integer[1];
726: sel[0] = new Integer(selectedRow);
727:
728: return sel;
729: }
730:
731: /**
732: * Check if the component has rendered yet.
733: * @return true if rendered at least once
734: */
735: boolean hasRendered() {
736: return rendered;
737: }
738:
739: transient ItemListener itemListener;
740: }
|