001: /*
002: * Beryl - A web platform based on XML, XSLT and Java
003: * This file is part of the Beryl XML GUI
004: *
005: * Copyright (C) 2004 Wenzel Jakob <wazlaf@tigris.org>
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011:
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-3107 USA
020: */
021:
022: package org.beryl.gui.widgets;
023:
024: import java.awt.Component;
025: import java.awt.Point;
026: import java.awt.event.MouseAdapter;
027: import java.awt.event.MouseEvent;
028: import java.util.ArrayList;
029:
030: import javax.swing.AbstractCellEditor;
031: import javax.swing.JScrollPane;
032: import javax.swing.JTable;
033: import javax.swing.ListSelectionModel;
034: import javax.swing.event.ListSelectionEvent;
035: import javax.swing.event.ListSelectionListener;
036: import javax.swing.event.TableModelEvent;
037: import javax.swing.event.TableModelListener;
038: import javax.swing.table.AbstractTableModel;
039: import javax.swing.table.JTableHeader;
040: import javax.swing.table.TableCellEditor;
041: import javax.swing.table.TableCellRenderer;
042: import javax.swing.table.TableModel;
043:
044: import org.beryl.gui.GUIEvent;
045: import org.beryl.gui.GUIEventListener;
046: import org.beryl.gui.GUIException;
047: import org.beryl.gui.Widget;
048: import org.beryl.gui.WidgetInfo;
049: import org.beryl.gui.model.MapChangeEvent;
050: import org.beryl.gui.model.MapDataModel;
051: import org.beryl.gui.model.ModelChangeEvent;
052: import org.beryl.gui.model.ModelChangeListener;
053: import org.beryl.gui.model.TableChangeEvent;
054: import org.beryl.gui.model.TableDataModel;
055: import org.beryl.gui.model.TableRow;
056: import org.beryl.gui.table.TableSorter;
057:
058: public class Table extends Widget {
059: protected static WidgetInfo tableInfo = null;
060: private JTable table = null;
061: private JScrollPane scrollPane = null;
062: private TableSorter sorter = null;
063: private TableModel messageModel = null;
064: private TableDataModel tableDataModel = null;
065: private boolean sendEvents = true;
066: private boolean processEvents = true;
067: private ArrayList columnKeys = null;
068: private ArrayList columnLabels = null;
069: private ArrayList columnSizes = null;
070: private Class[] columnClasses = null;
071: private String indexKey = null;
072: private String valueKey = null;
073: private String message = null;
074: private JTableHeader tableHeader = null;
075:
076: static {
077: tableInfo = new WidgetInfo(Table.class, widgetInfo);
078: tableInfo.addProperty("indexkey", "string", "");
079: tableInfo.addProperty("valuekey", "string", "");
080: tableInfo.addProperty("verticalScrollBar", "bool",
081: Boolean.FALSE);
082: tableInfo.addProperty("horizontalScrollBar", "bool",
083: Boolean.FALSE);
084: tableInfo.addProperty("selectionMode", "enum",
085: "multiple_interval");
086: tableInfo.addEvent("rightclick");
087: tableInfo.addEvent("doubleclick");
088: };
089:
090: /**
091: * Custom table model to display information messages
092: */
093: private class MessageModel extends AbstractTableModel {
094: public int getColumnCount() {
095: return 1;
096: }
097:
098: public int getRowCount() {
099: return 1;
100: }
101:
102: public String getColumnName(int column) {
103: return "";
104: }
105:
106: public Object getValueAt(int rowIndex, int columnIndex) {
107: return message;
108: }
109: };
110:
111: /**
112: * The custom cell editor lets you add your own widgets as renderer
113: * components of a <tt>Table</tt>.
114: */
115:
116: private class CustomCellRenderer implements TableCellRenderer {
117: private Widget widget = null;
118:
119: public CustomCellRenderer(Widget widget) {
120: this .widget = widget;
121: }
122:
123: public Component getTableCellRendererComponent(JTable table,
124: Object value, boolean isSelected, boolean hasFocus,
125: int row, int column) {
126: try {
127: if (table.isRowSelected(row))
128: widget.setProperty("background", table
129: .getSelectionBackground());
130: else
131: widget.setProperty("background", table
132: .getBackground());
133: } catch (GUIException e) {
134: throw new RuntimeException(e);
135: }
136: return widget.getRealWidget();
137: }
138: }
139:
140: /**
141: * The custom cell editor lets you add your own widgets as editor
142: * components of a <tt>Table</tt>.
143: * The Table's data model must contain a key called "value" from which
144: * the edited value will be extracted
145: */
146:
147: private class CustomCellEditor extends AbstractCellEditor implements
148: TableCellEditor {
149: private Widget widget = null;
150:
151: private CustomCellEditor(Widget widget) {
152: this .widget = widget;
153: }
154:
155: public Component getTableCellEditorComponent(JTable table,
156: Object value, boolean isSelected, int row, int column) {
157: try {
158: widget.setProperty("background", table
159: .getSelectionBackground());
160: } catch (GUIException e) {
161: throw new RuntimeException(e);
162: }
163: return widget.getRealWidget();
164: }
165:
166: public Object getCellEditorValue() {
167: return widget.getDataModel().getValue("value");
168: }
169: };
170:
171: private class TableDataModelAdapter implements TableModel,
172: ModelChangeListener {
173: private TableDataModel model = null;
174: private ArrayList listeners = null;
175:
176: public TableDataModelAdapter(TableDataModel model) {
177: this .model = model;
178: model.addModelChangeListener(this );
179: listeners = new ArrayList();
180: }
181:
182: public int getColumnCount() {
183: return columnKeys.size();
184: }
185:
186: public int getRowCount() {
187: return model.getRowCount();
188: }
189:
190: public Object getValueAt(int row, int column) {
191: return model.getValue(row, (String) columnKeys.get(column));
192: }
193:
194: public void setValueAt(Object value, int row, int column) {
195: try {
196: model.setValue(row, (String) columnKeys.get(column),
197: value);
198: } catch (GUIException e) {
199: throw new RuntimeException(e);
200: }
201: }
202:
203: public boolean isCellEditable(int row, int column) {
204: return model.isEditable(row, (String) columnKeys
205: .get(column));
206: }
207:
208: public Class getColumnClass(int column) {
209: if (columnClasses != null)
210: return columnClasses[column];
211: return Object.class;
212: }
213:
214: public String getColumnName(int column) {
215: return (String) columnLabels.get(column);
216: }
217:
218: public void addTableModelListener(TableModelListener listener) {
219: listeners.add(listener);
220: }
221:
222: public void removeTableModelListener(TableModelListener listener) {
223: listeners.remove(listener);
224: }
225:
226: public void modelChanged(ModelChangeEvent e) {
227: if (message == null) {
228: TableChangeEvent tce = (TableChangeEvent) e;
229:
230: TableModelEvent event = new TableModelEvent(this , tce
231: .getFirstIndex(), tce.getLastIndex(),
232: columnKeys.indexOf(tce.getKey()), tce.getType());
233:
234: try {
235: sendEvents = false;
236: processEvents = false;
237:
238: for (int i = 0; i < listeners.size(); i++) {
239: ((TableModelListener) listeners.get(i))
240: .tableChanged(event);
241: }
242: synchronizeDataModel();
243: } catch (GUIException ex) {
244: throw new RuntimeException(ex);
245: } finally {
246: sendEvents = true;
247: processEvents = true;
248: }
249:
250: table.repaint();
251: }
252: }
253: }
254:
255: public Table(Widget parent, String name) throws GUIException {
256: super (parent, name);
257:
258: table = new JTable() {
259: /**
260: * Swing bugfix: Allow different renderers/editor for each column,
261: * removes the limitation of 1 renderer/editor per column
262: */
263: public TableCellRenderer getCellRenderer(int row, int column) {
264: if (message == null) {
265: int sortedRow = sorter.getRowForSortedRow(row);
266: try {
267: TableRow tableRow = tableDataModel
268: .getTableRow(sortedRow);
269: String key = (String) columnKeys.get(column);
270: if (tableRow.hasCustomRenderer(key)) {
271: Widget renderer = tableRow.getRenderer(
272: Table.this ,
273: getValueAt(row, column),
274: isCellSelected(row, column),
275: hasFocus(), tableRow, key);
276:
277: if (renderer != null) {
278: return new CustomCellRenderer(renderer);
279: }
280: }
281: } catch (GUIException e) {
282: /* There should be no exception here */
283: throw new RuntimeException(e);
284: }
285:
286: if (tableDataModel != null) {
287: Object object = tableDataModel.getValue(
288: sortedRow, (String) columnKeys
289: .get(column));
290: if (object != null) {
291: return getDefaultRenderer(object.getClass());
292: }
293: }
294: }
295:
296: /* Fall back to the JTable internal processing */
297: return super .getCellRenderer(row, column);
298: }
299:
300: public TableCellEditor getCellEditor(int row, int column) {
301: if (message == null) {
302: int sortedRow = sorter.getRowForSortedRow(row);
303:
304: try {
305: TableRow tableRow = tableDataModel
306: .getTableRow(sortedRow);
307: String key = (String) columnKeys.get(column);
308:
309: Widget editor = tableRow.getEditor(Table.this ,
310: getValueAt(row, column), tableDataModel
311: .getTableRow(sortedRow), key);
312:
313: if (editor != null) {
314: return new CustomCellEditor(editor);
315: }
316: } catch (GUIException e) {
317: /* There should be no exception here */
318: throw new RuntimeException(e);
319: }
320:
321: if (tableDataModel != null) {
322: Object object = tableDataModel.getValue(
323: sortedRow, (String) columnKeys
324: .get(column));
325: if (object != null) {
326: return getDefaultEditor(object.getClass());
327: }
328: }
329: }
330: /* Fall back to the JTable internal processing */
331: return super .getCellEditor(row, column);
332: }
333: };
334:
335: tableHeader = table.getTableHeader();
336: sorter = new TableSorter();
337: sorter.addMouseListenerToHeaderInTable(table);
338: messageModel = new MessageModel();
339: scrollPane = new JScrollPane(table);
340: columnKeys = new ArrayList();
341: columnLabels = new ArrayList();
342: columnSizes = new ArrayList();
343:
344: table.getSelectionModel().addListSelectionListener(
345: new ListSelectionListener() {
346: public void valueChanged(ListSelectionEvent e) {
347: try {
348: if (sendEvents && !e.getValueIsAdjusting()) {
349: synchronizeDataModel();
350: }
351: } catch (GUIException ex) {
352: throw new RuntimeException(ex);
353: }
354: }
355: });
356: table
357: .setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
358: table.setRowSelectionAllowed(true);
359: table.setColumnSelectionAllowed(false);
360: }
361:
362: /**
363: * This lets you hide the current table and display a message instead.
364: * A call with a null message makes the table visible again. Use this
365: * method to indicate for example that there were "No Results" for
366: * a database query
367: */
368:
369: public void setMessage(String message) {
370: this .message = message;
371:
372: if (message == null) {
373: table.setTableHeader(tableHeader);
374: table.setModel(sorter);
375: } else if (message != null) {
376: table.setTableHeader(null);
377: table.setModel(messageModel);
378: }
379: }
380:
381: public void setColumnClasses(Class columnClasses[]) {
382: this .columnClasses = columnClasses;
383: }
384:
385: private void synchronizeDataModel() throws GUIException {
386: MapDataModel model = getDataModel();
387: if (model == null)
388: return;
389:
390: if (indexKey != null) {
391: int indices[] = table.getSelectedRows();
392: for (int i = 0; i < indices.length; i++) {
393: indices[i] = sorter.getRowForSortedRow(indices[i]);
394: }
395: model.setValue(Table.this , indexKey, indices);
396: }
397:
398: if (valueKey != null) {
399: int indices[] = table.getSelectedRows();
400: TableRow rows[] = new TableRow[indices.length];
401: for (int i = 0; i < indices.length; i++) {
402: rows[i] = tableDataModel.getTableRow(sorter
403: .getRowForSortedRow(indices[i]));
404: }
405: model.setValue(Table.this , valueKey, rows);
406: }
407: }
408:
409: /**
410: * This stops all TableEditor instances and saves the edited data
411: * to the table data model
412: */
413: public void stopEditors() {
414: table.editingStopped(null); /* This is necessary to ensure the changes won't be discarded */
415: }
416:
417: public void setTableDataModel(TableDataModel tableDataModel)
418: throws GUIException {
419: ModelChangeEvent event = new ModelChangeEvent(this ,
420: tableDataModel);
421: this .tableDataModel = tableDataModel;
422: try {
423: sendEvents = false;
424: stopEditors();
425: TableDataModelAdapter adapter = new TableDataModelAdapter(
426: tableDataModel);
427: sorter.setModel(adapter);
428: table.setModel(sorter);
429: } finally {
430: sendEvents = true;
431: }
432: /* Reload data model information */
433: modelChanged(event);
434:
435: for (int i = 0; i < columnSizes.size(); i++) {
436: Integer size = (Integer) columnSizes.get(i);
437: if (size != null) {
438: table.getColumnModel().getColumn(i).setPreferredWidth(
439: size.intValue());
440: }
441: }
442: }
443:
444: public TableDataModel getTableDataModel() {
445: return this .tableDataModel;
446: }
447:
448: public void setProperty(String name, Object value)
449: throws GUIException {
450: if (name.startsWith("column.")) {
451: String key = name.substring(7, name.length());
452: if (columnKeys.contains(key)) {
453: columnLabels.set(columnKeys.indexOf(key),
454: (String) value);
455: } else {
456: columnKeys.add(key);
457: columnLabels.add((String) value);
458: columnSizes.add(null);
459: }
460: } else if (name.startsWith("columnsize.")) {
461: int column = columnKeys.indexOf(name.substring(11, name
462: .length()));
463: if (column != -1) {
464: columnSizes.set(column, value);
465: } else {
466: throw new GUIException(
467: "Invalid column name while trying to set column size");
468: }
469: } else if ("verticalScrollBar".equals(name)) {
470: scrollPane
471: .setVerticalScrollBarPolicy(((Boolean) value)
472: .booleanValue() ? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS
473: : JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
474: } else if ("horizontalScrollBar".equals(name)) {
475: scrollPane
476: .setHorizontalScrollBarPolicy(((Boolean) value)
477: .booleanValue() ? JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS
478: : JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
479: } else if ("indexkey".equals(name)) {
480: indexKey = (String) value;
481: } else if ("valuekey".equals(name)) {
482: valueKey = (String) value;
483: } else {
484: super .setProperty(name, value);
485: }
486: }
487:
488: private void setSelectionValues(TableRow[] values) {
489: ListSelectionModel model = table.getSelectionModel();
490: if (values == null) {
491: table.clearSelection();
492: } else {
493: table.clearSelection();
494: for (int i = 0; i < values.length; i++) {
495: int index = sorter.getSortedRowForRow(tableDataModel
496: .indexOf(values[i]));
497: table.addRowSelectionInterval(index, index);
498: }
499: }
500: }
501:
502: private void setSelectionIndices(int[] indices) {
503: ListSelectionModel model = table.getSelectionModel();
504: if (indices == null) {
505: table.clearSelection();
506: } else {
507: table.clearSelection();
508: for (int i = 0; i < indices.length; i++) {
509: int index = sorter.getSortedRowForRow(indices[i]);
510: table.addRowSelectionInterval(index, index);
511: }
512: }
513: }
514:
515: public void reload() throws GUIException {
516: MapDataModel model = getDataModel();
517: if (model != null) {
518: try {
519: processEvents = false;
520:
521: int[] indices = indexKey == null ? null : (int[]) model
522: .getValue(indexKey);
523: TableRow values[] = valueKey == null ? null
524: : (TableRow[]) model.getValue(valueKey);
525:
526: if (indices != null) {
527: setSelectionIndices(indices);
528: } else if (values != null) {
529: setSelectionValues(values);
530: }
531:
532: if (((values != null && indices == null) || (values == null && indices == null))
533: && indexKey != null) {
534: int indices2[] = table.getSelectedRows();
535: for (int i = 0; i < indices2.length; i++) {
536: indices2[i] = sorter
537: .getRowForSortedRow(indices[i]);
538: }
539: model.setValue(Table.this , indexKey, indices);
540: }
541:
542: if (((indices != null && values == null) || (values == null && indices == null))
543: && valueKey != null) {
544: int indices2[] = table.getSelectedRows();
545: TableRow rows[] = new TableRow[indices2.length];
546: for (int i = 0; i < indices2.length; i++)
547: rows[i] = tableDataModel.getTableRow(sorter
548: .getRowForSortedRow(indices2[i]));
549: model.setValue(Table.this , valueKey, rows);
550: }
551: } finally {
552: processEvents = true;
553: }
554: }
555: }
556:
557: public void modelChanged(ModelChangeEvent e) throws GUIException {
558: if (processEvents) {
559: try {
560: sendEvents = false;
561: if (e.getSource() == this ) {
562: try {
563: reload();
564: } catch (IllegalArgumentException ex) {
565: /* Ignore, table data model is not yet set */
566: } catch (ArrayIndexOutOfBoundsException ex) {
567: /* Ignore, table data model is not yet set */
568: }
569: } else if (e instanceof MapChangeEvent) {
570: MapChangeEvent event = (MapChangeEvent) e;
571: if (event.getKey() == null) {
572: reload();
573: } else if (event.getKey().equals(indexKey)) {
574: setSelectionIndices((int[]) ((MapChangeEvent) e)
575: .getNewValue());
576: try {
577: processEvents = false;
578: if (valueKey != null) {
579: int indices[] = table.getSelectedRows();
580: TableRow rows[] = new TableRow[indices.length];
581: for (int i = 0; i < indices.length; i++)
582: rows[i] = tableDataModel
583: .getTableRow(sorter
584: .getRowForSortedRow(indices[i]));
585: ((MapDataModel) event.getModel())
586: .setValue(Table.this , valueKey,
587: rows);
588: }
589: } finally {
590: processEvents = true;
591: }
592: } else if (event.getKey().equals(valueKey)) {
593: setSelectionValues((TableRow[]) ((MapChangeEvent) e)
594: .getNewValue());
595: try {
596: processEvents = false;
597: if (indexKey != null) {
598: int indices[] = table.getSelectedRows();
599: for (int i = 0; i < indices.length; i++) {
600: indices[i] = sorter
601: .getRowForSortedRow(indices[i]);
602: }
603: ((MapDataModel) event.getModel())
604: .setValue(Table.this , indexKey,
605: indices);
606: }
607: } finally {
608: processEvents = true;
609: }
610: }
611: }
612: } finally {
613: sendEvents = true;
614: }
615: }
616: }
617:
618: public void addListener(String event, final String name,
619: final GUIEventListener listener) throws GUIException {
620: if ("rightclick".equals(event)) {
621: table.addMouseListener(new MouseAdapter() {
622: public void mousePressed(MouseEvent me) {
623: Point p = me.getPoint();
624: int selRow = table.rowAtPoint(me.getPoint());
625: if (me.isPopupTrigger() && selRow != -1
626: && message == null) {
627: if (table.getSelectedRows().length < 2) {
628: table.clearSelection();
629: table.addRowSelectionInterval(selRow,
630: selRow);
631: }
632: listener.eventOccured(new GUIEvent(Table.this ,
633: name, me));
634: }
635: }
636:
637: public void mouseReleased(MouseEvent me) {
638: mousePressed(me);
639: }
640: });
641: } else if (event.equals("doubleclick")) {
642: table.addMouseListener(new MouseAdapter() {
643: public void mousePressed(MouseEvent me) {
644: if (me.getClickCount() == 2 && message == null) {
645: listener.eventOccured(new GUIEvent(Table.this ,
646: name, me));
647: }
648: }
649: });
650: } else {
651: super .addListener(event, name, listener);
652: }
653: }
654:
655: public Component getWidget() {
656: return scrollPane;
657: }
658:
659: public Component getRealWidget() {
660: return table;
661: }
662:
663: public WidgetInfo getWidgetInfo() {
664: return tableInfo;
665: }
666: }
|