/*
* Copyright (C) 2012 Timo Vesalainen
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.vesalainen.parsers.sql.dsql.ui;
import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Category;
import com.google.appengine.api.datastore.Email;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.datastore.Link;
import com.google.appengine.api.datastore.PhoneNumber;
import com.google.appengine.api.datastore.PostalAddress;
import com.google.appengine.api.datastore.Rating;
import com.google.appengine.api.datastore.ShortBlob;
import com.google.appengine.api.datastore.Text;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.util.Date;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ComboBoxModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.TransferHandler;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import org.vesalainen.parsers.magic.Magic;
import org.vesalainen.parsers.magic.Magic.MagicResult;
import org.vesalainen.parsers.sql.dsql.GObjectHelper;
/**
* @author Timo Vesalainen
*/
public class DSJTable extends JTable
{
private static final Pattern NUMERIC = Pattern.compile("[0-9\\,\\.\\- ]+");
private static final Magic magic = Magic.getInstance();
private Window owner;
public DSJTable(Object[][] rowData, Object[] columnNames)
{
super(rowData, columnNames);
init();
}
public DSJTable(Vector rowData, Vector columnNames)
{
super(rowData, columnNames);
init();
}
public DSJTable(int numRows, int numColumns)
{
super(numRows, numColumns);
init();
}
public DSJTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm)
{
super(dm, cm, sm);
init();
}
public DSJTable(TableModel dm, TableColumnModel cm)
{
super(dm, cm);
init();
}
public DSJTable(TableModel dm)
{
super(dm);
init();
}
public DSJTable()
{
init();
}
public void setOwner(Window owner)
{
this.owner = owner;
}
private void init()
{
MyTransferHandler myTransferHandler = new MyTransferHandler(getTransferHandler());
setTransferHandler(myTransferHandler);
ActionMap actionMap = getActionMap();
Action copyAction = MyTransferHandler.getCopyAction();
actionMap.put(I18n.get("COPY"), copyAction);
setAutoCreateRowSorter(true);
setRowSelectionAllowed(true);
setDragEnabled(true);
setDefaultEditor(ComboBoxModel.class, new ComboBoxModelCellEditor());
setDefaultEditor(GeoPt.class, new GoogleObjectCellEditor(GeoPt.class));
setDefaultEditor(ShortBlob.class, new ShortBlobCellEditor());
setDefaultEditor(Blob.class, new BlobCellEditor());
setDefaultEditor(Rating.class, new RatingCellEditor());
setDefaultEditor(PostalAddress.class, new GoogleObjectCellEditor(PostalAddress.class));
setDefaultEditor(PhoneNumber.class, new GoogleObjectCellEditor(PhoneNumber.class));
setDefaultEditor(Link.class, new GoogleObjectCellEditor(Link.class));
setDefaultEditor(Text.class, new TextCellEditor());
setDefaultEditor(Email.class, new GoogleObjectCellEditor(Email.class));
setDefaultEditor(Category.class, new GoogleObjectCellEditor(Category.class));
setDefaultRenderer(ComboBoxModel.class, new ComboBoxModelCellRenderer());
setDefaultRenderer(GeoPt.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(ShortBlob.class, new ShortBlobTableCellRenderer());
setDefaultRenderer(Blob.class, new BlobTableCellRenderer());
setDefaultRenderer(Rating.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(PostalAddress.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(PhoneNumber.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(Link.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(Text.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(Email.class, new GoogleObjectTableCellRenderer());
setDefaultRenderer(Category.class, new GoogleObjectTableCellRenderer());
}
@Override
public void print(Graphics g)
{
int totalColumnWidth = columnModel.getTotalColumnWidth();
Graphics2D gg = (Graphics2D) g;
FontRenderContext fontRenderContext = gg.getFontRenderContext();
for (int col = 0; col < columnModel.getColumnCount(); col++)
{
TableColumn column = columnModel.getColumn(col);
int max = 0;
boolean numeric = true;
for (int row = 0; row < getRowCount(); row++)
{
Object value = dataModel.getValueAt(row, col);
String str = value != null ? getString(value) : "";
Matcher matcher = NUMERIC.matcher(str);
if (!matcher.matches())
{
numeric = false;
}
TableCellRenderer cellRenderer = getCellRenderer(row, col);
Component component = cellRenderer.getTableCellRendererComponent(this, value, false, false, row, col);
Font font = component.getFont();
Rectangle2D stringBounds = font.getStringBounds(str, fontRenderContext);
max = Math.max(max, (int) (1.5 * stringBounds.getWidth()));
}
if (numeric)
{
column.setMaxWidth(max);
}
else
{
column.setMinWidth(0);
column.setMaxWidth(max);
}
}
int left = totalColumnWidth - columnModel.getTotalColumnWidth();
int hiddenTotal = 0;
for (int col = 0; col < columnModel.getColumnCount(); col++)
{
TableColumn column = columnModel.getColumn(col);
hiddenTotal += column.getMaxWidth() - column.getWidth();
}
float ratio = (float) left / (float) hiddenTotal;
for (int col = 0; col < columnModel.getColumnCount(); col++)
{
TableColumn column = columnModel.getColumn(col);
int hidden = column.getMaxWidth() - column.getWidth();
if (hidden > 0)
{
column.setMinWidth(column.getWidth() + (int) (ratio * (float) hidden));
}
}
totalColumnWidth = columnModel.getTotalColumnWidth();
revalidate();
super.paint(g);
}
private String getString(Object value)
{
if (value instanceof Date)
{
DateFormat df = DateFormat.getDateTimeInstance();
return df.format(value);
}
return GObjectHelper.getString(value);
}
private static class MyTransferHandler extends TransferHandler implements ClipboardOwner
{
private TransferHandler transferHandler;
public MyTransferHandler(TransferHandler transferHandler)
{
this.transferHandler = transferHandler;
}
@Override
protected Transferable createTransferable(JComponent c)
{
return new MyTransferable((JTable) c);
}
@Override
public void setDragImage(Image img)
{
transferHandler.setDragImage(img);
}
@Override
public Image getDragImage()
{
return transferHandler.getDragImage();
}
@Override
public void setDragImageOffset(Point p)
{
transferHandler.setDragImageOffset(p);
}
@Override
public Point getDragImageOffset()
{
return transferHandler.getDragImageOffset();
}
@Override
public void exportAsDrag(JComponent comp, InputEvent e, int action)
{
transferHandler.exportAsDrag(comp, e, action);
}
@Override
public void exportToClipboard(JComponent comp, Clipboard clip, int action) throws IllegalStateException
{
Transferable transferable = createTransferable(comp);
clip.setContents(transferable, this);
}
@Override
public boolean importData(TransferSupport support)
{
return transferHandler.importData(support);
}
@Override
public boolean importData(JComponent comp, Transferable t)
{
return transferHandler.importData(comp, t);
}
@Override
public boolean canImport(TransferSupport support)
{
return transferHandler.canImport(support);
}
@Override
public boolean canImport(JComponent comp, DataFlavor[] transferFlavors)
{
return transferHandler.canImport(comp, transferFlavors);
}
@Override
public int getSourceActions(JComponent c)
{
return transferHandler.getSourceActions(c);
}
@Override
public Icon getVisualRepresentation(Transferable t)
{
return transferHandler.getVisualRepresentation(t);
}
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents)
{
}
}
private static class MyTransferable implements Transferable
{
private static DataFlavor[] flavors;
static
{
try
{
flavors = new DataFlavor[]
{
new DataFlavor("text/html;class=java.lang.String"),
new DataFlavor("text/html;class=java.io.Reader"),
new DataFlavor("text/html;charset=utf-8;class=java.io.InputStream"),
new DataFlavor("text/plain;class=java.lang.String"),
new DataFlavor("text/plain;class=java.io.Reader"),
new DataFlavor("text/plain;charset=utf-8;class=java.io.InputStream"),
DataFlavor.stringFlavor
};
}
catch (ClassNotFoundException ex)
{
throw new UnsupportedOperationException(ex);
}
}
private final String htmlData;
private final String plainData;
public MyTransferable(JTable table)
{
StringBuilder html = new StringBuilder();
html.append("<html><body><table>");
StringBuilder plain = new StringBuilder();
boolean selectAll = table.getSelectedRowCount() == table.getRowCount();
int columnCount = table.getColumnCount();
if (selectAll)
{
html.append("<thead><tr>");
for (int ii = 0; ii < columnCount; ii++)
{
String columnName = table.getColumnName(ii);
html.append("<th>");
html.append(columnName);
html.append("</th>");
plain.append(columnName + "\t");
}
html.append("</tr></thead>");
plain.append("\n");
}
html.append("<tbody>");
int[] selectedRows = table.getSelectedRows();
TableModel model = table.getModel();
for (int row : selectedRows)
{
row = table.convertRowIndexToModel(row);
html.append("<tr>");
for (int col = 0; col < columnCount; col++)
{
html.append("<td>");
Object value = model.getValueAt(row, col);
if (value != null)
{
populate(html, plain, value);
}
html.append("</td>");
plain.append("\t");
}
html.append("</tr>");
plain.append("\n");
}
html.append("</tbody></table></body><html>");
htmlData = html.toString();
plainData = plain.toString();
}
@Override
public DataFlavor[] getTransferDataFlavors()
{
return flavors;
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor)
{
for (DataFlavor df : flavors)
{
if (df.equals(flavor))
{
return true;
}
}
return false;
}
@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
{
String data = null;
if (flavor.getMimeType().startsWith("text/html"))
{
data = htmlData;
}
else
{
if (flavor.getMimeType().startsWith("text/plain"))
{
data = plainData;
}
else
{
throw new UnsupportedFlavorException(flavor);
}
}
switch (flavor.getRepresentationClass().getCanonicalName())
{
case "java.lang.String":
return data;
case "java.io.Reader":
return new StringReader(data);
case "java.io.InputStream":
return new ByteArrayInputStream(data.getBytes("utf-8"));
default:
throw new UnsupportedFlavorException(flavor);
}
}
private void populate(StringBuilder html, StringBuilder plain, Object value)
{
html.append(GObjectHelper.getString(value));
plain.append(GObjectHelper.getString(value));
}
}
public class ComboBoxModelCellEditor extends AbstractCellEditor implements TableCellEditor
{
private JComboBox combo = new JComboBox();
@Override
public Object getCellEditorValue()
{
return combo.getModel();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
if (value != null)
{
ComboBoxModel model = (ComboBoxModel) value;
combo.setModel(model);
}
else
{
combo.setModel(null);
}
return combo;
}
}
public class GoogleObjectCellEditor extends AbstractCellEditor implements TableCellEditor
{
private Class<?> type;
private JTextField editor = new JTextField();
public GoogleObjectCellEditor(Class<?> type)
{
this.type = type;
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
if (value != null)
{
editor.setText(GObjectHelper.getString(value));
}
else
{
editor.setText(null);
}
return editor;
}
@Override
public Object getCellEditorValue()
{
String text = editor.getText();
if (text != null && !text.isEmpty())
{
return GObjectHelper.valueOf(type, text);
}
else
{
return null;
}
}
}
public class ShortBlobCellEditor extends BlobCellEditor
{
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
columnName = table.getColumnName(column);
if (value != null)
{
ShortBlob blob = (ShortBlob) value;
dialog.setBytes(blob.getBytes());
button.setText(dialog.getContentDescription());
}
else
{
button.setText(null);
}
return button;
}
@Override
public Object getCellEditorValue()
{
byte[] bytes = dialog.getBytes();
if (bytes != null)
{
return new ShortBlob(bytes);
}
else
{
return null;
}
}
}
public class BlobCellEditor extends AbstractCellEditor implements TableCellEditor, ActionListener
{
protected static final String EDIT = "edit";
protected JButton button = new JButton();
protected BytesDialog dialog = new BytesDialog(owner);
protected MagicResult guess;
protected String columnName;
public BlobCellEditor()
{
button.setActionCommand(EDIT);
button.addActionListener(this);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
columnName = table.getColumnName(column);
if (value != null)
{
Blob blob = (Blob) value;
dialog.setBytes(blob.getBytes());
button.setText(dialog.getContentDescription());
}
else
{
button.setText(null);
}
return button;
}
@Override
public Object getCellEditorValue()
{
byte[] bytes = dialog.getBytes();
if (bytes != null)
{
return new Blob(bytes);
}
else
{
return null;
}
}
@Override
public void actionPerformed(ActionEvent e)
{
if (EDIT.equals(e.getActionCommand()))
{
if (dialog.input())
{
button.setText(dialog.getContentDescription());
}
fireEditingStopped();
}
}
}
public class RatingCellEditor extends AbstractCellEditor implements TableCellEditor
{
private JSpinner editor;
public RatingCellEditor()
{
SpinnerNumberModel model = new SpinnerNumberModel(0, Rating.MIN_VALUE, Rating.MAX_VALUE, 1);
editor = new JSpinner(model);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
if (value != null)
{
Rating rating = (Rating) value;
editor.setValue(rating.getRating());
}
else
{
editor.setValue(0);
}
return editor;
}
@Override
public Object getCellEditorValue()
{
return new Rating((Integer) editor.getValue());
}
}
public class TextCellEditor extends AbstractCellEditor implements TableCellEditor, ActionListener
{
private static final String EDIT = "edit";
private JButton button = new JButton();
private TextDialog dialog = new TextDialog(owner);
public TextCellEditor()
{
button.setActionCommand(EDIT);
button.addActionListener(this);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
if (value != null)
{
Text text = (Text) value;
button.setText(text.getValue());
}
else
{
button.setText(null);
}
return button;
}
@Override
public Object getCellEditorValue()
{
return new Text(button.getText());
}
@Override
public void actionPerformed(ActionEvent e)
{
if (EDIT.equals(e.getActionCommand()))
{
dialog.setText(button.getText());
if (dialog.input())
{
button.setText(dialog.getText());
}
fireEditingStopped();
}
}
}
public class ComboBoxModelCellRenderer extends TooltippedTableCellRenderer
{
@Override
protected void setValue(Object value)
{
if (value != null)
{
ComboBoxModel model = (ComboBoxModel) value;
super.setValue(model.getSelectedItem());
}
else
{
super.setValue(value);
}
}
}
public class GoogleObjectTableCellRenderer extends TooltippedTableCellRenderer
{
@Override
protected void setValue(Object value)
{
if (value != null)
{
super.setValue(GObjectHelper.getString(value));
}
else
{
super.setValue(value);
}
}
}
public class BlobTableCellRenderer extends TooltippedTableCellRenderer
{
@Override
protected void setValue(Object value)
{
if (value != null)
{
Blob blob = (Blob) value;
MagicResult guess;
try
{
guess = magic.guess(blob.getBytes());
}
catch (IOException ex)
{
throw new IllegalArgumentException(ex);
}
if (guess != null)
{
super.setValue(guess.getDescription());
}
else
{
super.setValue("???");
}
}
else
{
super.setValue(value);
}
}
}
public class ShortBlobTableCellRenderer extends TooltippedTableCellRenderer
{
@Override
protected void setValue(Object value)
{
if (value != null)
{
ShortBlob blob = (ShortBlob) value;
MagicResult guess;
try
{
guess = magic.guess(blob.getBytes());
}
catch (IOException ex)
{
throw new IllegalArgumentException(ex);
}
if (guess != null)
{
super.setValue(guess.getDescription());
}
else
{
super.setValue("???");
}
}
else
{
super.setValue(value);
}
}
}
public class TooltippedTableCellRenderer extends DefaultTableCellRenderer
{
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setToolTipText(getText());
return component;
}
}
}