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