/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.ui;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import org.w3c.dom.Element;
import util.*;
import util.xml.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.BugException;
/** Combo box */
public class TableImp extends JScrollPane
{
private JComponent wrapper;
private JTable t;
private OurTableModel otm;
private boolean inSetSelect = false;
/** Callbacks */
private String callbackEditing = null, callbackChange = null,
callbackSelect = null, callbackAction = null;
/** Margin on right side of table header text at min size */
private final static int HEADERRIGHTMARGIN = 2;
/** Extra space required (insets) around header columns in addition to text width */
private static int headerExtraWidth = -1;
/** Extra left indent on this table, used to make things line up on Mac */
private int macIndent;
/** Interface */
private Table publicInterface;
/** Map of flags for cells (TableLocation -> Flag) */
private Map<TableLocation, Flag> flags = new HashMap<TableLocation, Flag>();
/** Standard colours */
private static Color
cNormal = UIManager.getColor("Table.foreground"),
cNormalSelected = UIManager.getColor("Table.selectionForeground"),
cDim = dim(cNormal),
cDimSelected = dim(cNormalSelected),
cNormalBG = UIManager.getColor("Table.background"),
cSelectedBG = UIManager.getColor("Table.selectionBackground");
// TODO Is there a better way to get this colour? Shared TableImp, EditBoxImp
private static Color cErrorBG = new Color(255, 200, 200);
TableImp()
{
if(headerExtraWidth==-1)
{
try
{
JWindow w = new JWindow();
JTable t = new JTable();
JScrollPane sp = new JScrollPane(t);
w.getContentPane().setLayout(new BorderLayout());
w.getContentPane().add(sp, BorderLayout.CENTER);
OurTableModel model = new OurTableModel(new Element[] {
XML.parse("<what name='test' type='string' width='0'/>").getDocumentElement()
});
t.setModel(model);
t.getColumnModel().getColumn(0).setMaxWidth(25);
w.setSize(0, 0);
w.setVisible(true);
JLabel headerLabel = (JLabel)t.getTableHeader().getDefaultRenderer();
headerExtraWidth = headerLabel.getInsets().left+headerLabel.getInsets().right;
w.dispose();
}
catch(XMLException e)
{
// Not possible!
throw new Error("Impossible error checking table widths");
}
}
setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS);
t = new JTable();
t.setDefaultRenderer(String.class, new FlaggedCellRenderer());
t.setDefaultEditor(String.class, new CheckedCellEditor());
// Make headers left-aligned (they are in other Mac apps, plus it
// looks much better anyhow) - note this is default on Mac now
JLabel headerRenderer = ((JLabel)t.getTableHeader().getDefaultRenderer());
headerRenderer.setHorizontalAlignment(JLabel.LEADING);
// Selection listener
t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
t.getSelectionModel().addListSelectionListener(new ListSelectionListener()
{
@Override
public void valueChanged(ListSelectionEvent e)
{
if(callbackSelect!=null && !inSetSelect)
getInterface().getOwner().getCallbackHandler().callHandleErrors(callbackSelect);
}
});
// Double-clicks
t.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
if(e.getClickCount()==2 && callbackAction!=null)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(callbackAction);
}
}
});
t.getActionMap().put("pressedreturn", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
if(callbackAction!=null)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(callbackAction);
}
}
});
t.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "pressedreturn");
setViewportView(t);
publicInterface = new TableInterface();
wrapper = new JPanel(new BorderLayout());
wrapper.setOpaque(false);
wrapper.add(this, BorderLayout.CENTER);
}
/** @return List interface */
public Table getInterface() { return publicInterface; }
private class OurTableModel extends AbstractTableModel
{
private class Col
{
Class<?> type;
String name;
boolean editable;
int preferredWidth = -1;
}
private Col[] cols;
private LinkedList<Object[]> data = new LinkedList<Object[]>();
OurTableModel(Element[] elements)
{
cols = new Col[elements.length];
for(int i=0; i<elements.length; i++)
{
cols[i] = new Col();
String type = elements[i].getAttribute("type");
if(type.equals("string"))
{
cols[i].type = String.class;
}
else if(type.equals("boolean"))
{
cols[i].type = Boolean.class;
}
else
{
throw new BugException("Unknown type=: "+type);
}
cols[i].name = elements[i].getAttribute("name");
cols[i].editable = "y".equals(elements[i].getAttribute("editable"));
if(elements[i].hasAttribute("width"))
{
String width = elements[i].getAttribute("width");
try
{
cols[i].preferredWidth = Integer.parseInt(width);
}
catch(NumberFormatException nfe)
{
throw new BugException("Invalid width=: "+width);
}
}
}
}
void setColumnWidths(JTable t)
{
Font f = t.getTableHeader().getFont();
for(int i=0; i<cols.length; i++)
{
TableColumn col = t.getColumnModel().getColumn(i);
int width = cols[i].preferredWidth;
if(width >= 0)
{
int headerWidth = Math.round((float)f.getStringBounds(
t.getColumnName(i), GraphicsUtils.getFontRenderContext()).getWidth());
width = Math.max(width, headerWidth + headerExtraWidth + HEADERRIGHTMARGIN);
col.setPreferredWidth(width);
col.setMinWidth(width);
col.setMaxWidth(width);
}
}
}
@Override
public int getRowCount()
{
return data.size();
}
@Override
public int getColumnCount()
{
return cols.length;
}
@Override
public Object getValueAt(int row, int col)
{
Object[] rowValues = data.get(row);
return rowValues[col];
}
@Override
public void setValueAt(Object value, int row, int col)
{
internalSetValueAt(value, row, col, true);
}
private void internalSetValueAt(Object value, int row, int col, boolean user)
{
Object[] rowData = data.get(row);
if(rowData[col].equals(value))
{
return; // Do nothing if no change
}
Object before = rowData[col];
rowData[col] = value;
if(!user)
{
fireTableDataChanged();
}
if(user && callbackChange!=null)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(
callbackChange, row, col, before);
}
}
@Override
public Class<?> getColumnClass(int col)
{
return cols[col].type;
}
@Override
public String getColumnName(int col)
{
return cols[col].name;
}
@Override
public boolean isCellEditable(int row, int col)
{
if(!cols[col].editable) return false;
Flag f = flags.get(new TableLocation(row, col));
return f==null || f.editable;
}
int add()
{
Object[] newRow = new Object[getColumnCount()];
for(int i=0; i<cols.length; i++)
{
if(cols[i].type==String.class)
{
newRow[i] = "";
}
else if(cols[i].type==Boolean.class)
{
newRow[i] = Boolean.FALSE;
}
else
{
throw new Error("ar?");
}
}
data.add(newRow);
int newIndex = data.size()-1;
fireTableDataChanged();
return newIndex;
}
void remove(int index)
{
// Get rid of it from linkedlist
data.remove(index);
// Shuffle up the flags
Map<TableLocation, Flag> newFlags = new TreeMap<TableLocation, Flag>();
for(Iterator<Map.Entry<TableLocation, Flag>> i=flags.entrySet().iterator();
i.hasNext(); )
{
Map.Entry<TableLocation, Flag> me = i.next();
TableLocation tl = me.getKey();
if(tl.index >= index)
{
i.remove(); // Remove items on same row
if(tl.index != index)
{ // Other ones are shuffled up via a new map (can't modify this one
// while iterating on it)
newFlags.put(new TableLocation(tl.index-1, tl.column), me.getValue());
}
}
}
flags.putAll(newFlags);
// Fire change
fireTableDataChanged();
}
void setString(int index, int column, String value)
{
checkExists(index, column);
checkType(column, String.class, "string");
internalSetValueAt(value, index, column, false);
}
String getString(int index, int column)
{
checkExists(index, column);
checkType(column, String.class, "string");
return (String)getValueAt(index, column);
}
void setBoolean(int index, int column, boolean value)
{
checkExists(index, column);
checkType(column, Boolean.class, "boolean");
internalSetValueAt(Boolean.valueOf(value), index, column, false);
}
boolean getBoolean(int index, int column)
{
checkExists(index, column);
checkType(column, Boolean.class, "Boolean");
return ((Boolean)getValueAt(index, column)).booleanValue();
}
/**
* Checks whether a given index and column are valid.
* @param index
* @param column
* @throws BugException
*/
private void checkExists(int index, int column)
{
if(index >= getRowCount() || column >= getColumnCount() ||
index<0 || column<0)
{
throw new BugException("Row or column out of range");
}
}
/**
* Checks whether a given column is required type
* @param column Column number
* @param type Type class
* @param typeName Type name for use in error
* @throws BugException
*/
private void checkType(int column, Class<?> type, String typeName)
{
if(cols[column].type!=type)
{
throw new BugException("Column is not " + typeName + " type");
}
}
public void setEditable(int index, int column, boolean editable)
{
checkExists(index, column);
if(!cols[column].editable)
{
throw new BugException("Cannot change editable state of non-editable columns");
}
TableLocation tl = new TableLocation(index, column);
// Get existing flag, if any
Flag f = flags.get(tl);
if(f==null)
{
if(editable)
{
return;
}
f = new Flag();
flags.put(tl, f);
}
// Set new flag
f.editable = editable;
if(f.canDiscard())
{
flags.remove(tl);
}
// Repaint
t.repaint();
}
public boolean isEditable(int index, int column)
{
checkExists(index, column);
if(!cols[column].editable)
{
return false;
}
TableLocation tl = new TableLocation(index, column);
Flag f = flags.get(tl);
return f==null || f.editable;
}
public void setOverwrite(int index, int column, boolean overwrite)
{
checkExists(index, column);
checkType(column, String.class, "string");
TableLocation tl = new TableLocation(index, column);
// Get existing flag, if any
Flag f = flags.get(tl);
if(f==null)
{
if(!overwrite)
{
return;
}
f = new Flag();
flags.put(tl, f);
}
// Set new flag
f.overwrite = overwrite;
if(f.canDiscard())
flags.remove(tl);
// Repaint
t.repaint();
}
public boolean isOverwrite(int index, int column)
{
checkExists(index, column);
if(!cols[column].editable)
{
return false;
}
TableLocation tl = new TableLocation(index, column);
Flag f = flags.get(tl);
return f!=null && f.overwrite;
}
public void setDim(int index, int column, boolean dim)
{
checkExists(index, column);
checkType(column, String.class, "string");
TableLocation tl = new TableLocation(index, column);
// Get existing flag, if any
Flag f = flags.get(tl);
if(f==null)
{
if(!dim)
{
return;
}
f = new Flag();
flags.put(tl, f);
}
// Set new flag
f.dim = dim;
if(f.canDiscard())
flags.remove(tl);
// Repaint
t.repaint();
}
public boolean isDim(int index, int column)
{
checkExists(index, column);
TableLocation tl = new TableLocation(index, column);
Flag f = flags.get(tl);
return f!=null && f.dim;
}
}
private static class Flag
{
boolean editable = true;
boolean overwrite = false;
boolean dim = false;
/**
* Checks if the flag has default values, meaning it can be discarded.
* @return True if flag can be discarded
*/
boolean canDiscard() { return editable && !overwrite && !dim; }
}
private static Color dim(Color original)
{
return new Color(
original.getRed(), original.getGreen(), original.getBlue(), 128);
}
private class FlaggedCellRenderer extends DefaultTableCellRenderer
{
@Override
public Component getTableCellRendererComponent(JTable t, Object o,
boolean selected, boolean focus, int index, int column)
{
Component c = super.getTableCellRendererComponent(
t, o, selected, focus, index, column);
TableLocation tl = new TableLocation(index, column);
Flag f = flags.get(tl);
if(f==null || (f.editable && !f.dim))
{
c.setForeground(selected ? cNormalSelected : cNormal);
}
else
{
c.setForeground(selected ? cDimSelected : cDim);
}
c.setBackground(selected ? cSelectedBG : cNormalBG);
return c;
}
}
/** Represents a location in table. Suitable for use as key in map */
private class TableLocation implements Comparable<TableLocation>
{
private int index, column;
TableLocation(int index, int column)
{
this.index = index;
this.column = column;
}
@Override
public boolean equals(Object o)
{
if(!(o instanceof TableLocation))
{
return false;
}
TableLocation tl = (TableLocation)o;
return tl.index==index && tl.column==column;
}
@Override
public int hashCode()
{
return (index + ":" + column).hashCode();
}
@Override
public int compareTo(TableLocation tl)
{
if(tl.index < index)
{
return 1;
}
if(tl.index > index)
{
return -1;
}
if(tl.column < column)
{
return 1;
}
if(tl.column > column)
{
return -1;
}
return 0;
}
}
class CheckedCellEditor extends JTextField implements TableCellEditor, ActionListener
{
/** Current and initial values */
private String value, originalValue;
/** True when programmatically setting value */
private boolean inSet = false;
/** Current index/col editing */
private int index, col;
/** True if item is currently invalid (red) */
private boolean invalid = false;
/** True if item is currently dim (grey) */
private boolean dim = false;
/** Listeners */
private LinkedList<CellEditorListener> listeners =
new LinkedList<CellEditorListener>();
CheckedCellEditor()
{
addActionListener(this);
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
getActionMap().put("cancel", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent ae)
{
cancelCellEditing();
}
});
getDocument().addDocumentListener(new DocumentListener()
{
@Override
public void insertUpdate(DocumentEvent arg0)
{
changed();
}
@Override
public void removeUpdate(DocumentEvent arg0)
{
changed();
}
@Override
public void changedUpdate(DocumentEvent arg0)
{
changed();
}
});
addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
stopCellEditing();
}
});
setBorder(BorderFactory.createEmptyBorder(
0, t.getColumnModel().getColumnMargin(),
0, 0));
setFont(t.getFont());
}
@Override
public void actionPerformed(ActionEvent ae)
{
stopCellEditing();
}
private void changed()
{
value = getText();
if(callbackEditing!=null && !inSet)
{
Table.EditingControl ec = new Table.EditingControl();
getInterface().getOwner().getCallbackHandler().callHandleErrors(
callbackEditing, index, col, value, ec);
invalid = ec.isError();
dim = ec.isDim();
updateColours();
}
}
@Override
public Component getTableCellEditorComponent(JTable t, Object o,
boolean selected, int index, int col)
{
value = originalValue = (String)o;
invalid = false;
dim = false;
this.index = index;
this.col = col;
try
{
inSet = true;
setText(value);
}
finally
{
inSet = false;
}
TableLocation tl = new TableLocation(index, col);
Flag f = flags.get(tl);
if(f!=null && f.overwrite)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
selectAll();
}
});
}
updateColours();
return this;
}
private void updateColours()
{
if(invalid)
{
setForeground(cNormal);
setBackground(cErrorBG);
}
else if(dim)
{
setForeground(cDim);
setBackground(cNormalBG);
}
else
{
setForeground(cNormal);
setBackground(cNormalBG);
}
}
@Override
public Object getCellEditorValue()
{
if(invalid)
{
return originalValue;
}
else
{
return value;
}
}
@Override
public boolean isCellEditable(EventObject e)
{
return true;
}
@Override
public boolean shouldSelectCell(EventObject e)
{
return true;
}
@Override
public boolean stopCellEditing()
{
// Send to listeners
CellEditorListener[] listenersArray =
listeners.toArray(new CellEditorListener[listeners.size()]);
ChangeEvent ce = new ChangeEvent(this);
for(int i=0; i<listenersArray.length; i++)
{
listenersArray[i].editingStopped(ce);
}
return true;
}
@Override
public void cancelCellEditing()
{
value = originalValue;
// Send to listeners
CellEditorListener[] listenersArray =
listeners.toArray(new CellEditorListener[listeners.size()]);
ChangeEvent ce = new ChangeEvent(this);
for(int i=0; i<listenersArray.length; i++)
{
listenersArray[i].editingCanceled(ce);
}
}
@Override
public void addCellEditorListener(CellEditorListener listener)
{
listeners.add(listener);
}
@Override
public void removeCellEditorListener(CellEditorListener listener)
{
listeners.remove(listener);
}
}
/** Class implementing combo interface */
class TableInterface extends BasicWidget implements Table, InternalWidget
{
int width = -1;
private boolean multiSelect = false;
@Override
public int getContentType()
{
return CONTENT_NONE;
}
public TableInterface()
{
setRows(4);
}
@Override
public void addXMLChild(String slotName, Widget child)
{
throw new BugException("Tables cannot contain children");
}
@Override
public String[] getReservedChildren()
{
return new String[] { "column" };
}
@Override
public void setReservedData(Element[] elements)
{
otm = new OurTableModel(elements);
t.setModel(otm);
otm.setColumnWidths(t);
}
@Override
public JComponent getJComponent()
{
return wrapper;
}
@Override
public int getPreferredWidth()
{
if(width!=-1)
{
return width;
}
else
{
return getPreferredSize().width;
}
}
@Override
public int getPreferredHeight(int width)
{
return getPreferredSize().height;
}
@Override
public void setWidth(int width)
{
this.width = width;
}
@Override
public int addItem()
{
stopEditing();
return otm.add();
}
void stopEditing()
{
if(t.isEditing())
{
t.getCellEditor().stopCellEditing();
}
}
@Override
public void removeItem(int index)
{
stopEditing();
otm.remove(index);
}
@Override
public void clear()
{
stopEditing();
while(otm.getRowCount()>0)
otm.remove(0);
}
@Override
public void setString(int index, int column, String value)
{
otm.setString(index, column, value);
}
@Override
public String getString(int index, int column)
{
return otm.getString(index, column);
}
@Override
public void setBoolean(int index, int column, boolean value)
{
otm.setBoolean(index, column, value);
}
@Override
public boolean getBoolean(int index, int column)
{
return otm.getBoolean(index, column);
}
@Override
public void setOnChange(String callback)
{
getInterface().getOwner().getCallbackHandler().check(callback,
new Class[] {int.class, int.class, Object.class});
TableImp.this.callbackChange = callback;
}
@Override
public void setOnEditing(String callback)
{
getInterface().getOwner().getCallbackHandler().check(callback,
new Class[] {int.class, int.class, String.class, Table.EditingControl.class});
TableImp.this.callbackEditing = callback;
}
@Override
public void setOnSelect(String callback)
{
getInterface().getOwner().getCallbackHandler().check(callback);
TableImp.this.callbackSelect = callback;
}
@Override
public void setEditable(int index, int column, boolean editable)
{
otm.setEditable(index, column, editable);
}
@Override
public boolean isEditable(int index, int column)
{
return otm.isEditable(index, column);
}
@Override
public void setDim(int index, int column, boolean dim)
{
otm.setDim(index, column, dim);
}
@Override
public boolean isDim(int index, int column)
{
return otm.isDim(index, column);
}
@Override
public void setOverwrite(int index, int column, boolean overwrite)
{
otm.setOverwrite(index, column, overwrite);
}
@Override
public boolean isOverwrite(int index, int column)
{
return otm.isOverwrite(index, column);
}
@Override
public int getNumItems()
{
return otm.getRowCount();
}
@Override
public void setRows(int rows)
{
t.setPreferredScrollableViewportSize(new Dimension(
getPreferredWidth(), rows*(t.getRowHeight()+t.getRowMargin())
));
}
@Override
public int getSelectedIndex()
{
if(multiSelect)
{
throw new BugException("Can't call getSelectedIndex on a MultiSelect table");
}
return t.getSelectedRow();
}
@Override
public int[] getSelectedIndices()
{
return t.getSelectedRows();
}
@Override
public void setSelectedIndex(int selected)
{
try
{
inSetSelect = true;
t.getSelectionModel().clearSelection();
t.getSelectionModel().addSelectionInterval(selected, selected);
}
finally
{
inSetSelect = false;
}
}
@Override
public void setSelectedIndices(int[] selected)
{
try
{
t.getSelectionModel().clearSelection();
for(int i=0; i<selected.length; i++)
{
t.getSelectionModel().addSelectionInterval(selected[i], selected[i]+1);
}
}
finally
{
inSetSelect = false;
}
}
@Override
public void setMultiSelect(boolean multiSelect)
{
this.multiSelect = multiSelect;
t.setSelectionMode(multiSelect
? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
: ListSelectionModel.SINGLE_SELECTION);
}
@Override
public void setOnAction(String callback)
{
callbackAction = callback;
}
@Override
public void setMacIndent(boolean macIndent)
{
setMacIndent(macIndent ? SupportsMacIndent.TYPE_EDIT_LEGACY
: SupportsMacIndent.TYPE_NONE);
}
@Override
public void setMacIndent(String macIndent)
{
int newValue = UISingleton.getMacIndent(macIndent);
if(newValue != TableImp.this.macIndent)
{
TableImp.this.macIndent = newValue;
UISingleton.runInSwing(new Runnable()
{
@Override
public void run()
{
wrapper.setBorder(TableImp.this.macIndent == 0 ? null :
BorderFactory.createEmptyBorder(0, TableImp.this.macIndent, 0, 0));
}
});
}
}
}
}