/* Copyright (c) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.spreadsheet.gui;
import com.google.gdata.client.spreadsheet.ListQuery;
import com.google.gdata.client.spreadsheet.SpreadsheetService;
import com.google.gdata.data.TextContent;
import com.google.gdata.data.spreadsheet.CustomElementCollection;
import com.google.gdata.data.spreadsheet.ListEntry;
import com.google.gdata.data.spreadsheet.ListFeed;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.VersionConflictException;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.AbstractTableModel;
/**
* Widget for displaying and editing a spreadsheet.
*
* This is just for demonstrative purposes, it is not very featureful.
*
*
*/
public class ListBasedSpreadsheetPanel extends JPanel {
/** The Google Spreadsheets service. */
private SpreadsheetService service;
/** The list feed URL. */
private URL listFeedUrl;
/** The model that Swing uses to represent the table. */
private ListTableModel model;
/** The table widget. */
private JTable table;
private JButton refreshButton;
private JButton deleteOneButton;
private JButton revertOneButton;
private JButton commitOneButton;
private JButton commitAllButton;
private JTextField fulltextField;
private JTextField spreadsheetQueryField;
private JTextField orderbyField;
/**
* Creates a list-based spreadsheet editing panel.
* @param service the spreadsheet service
* @param listFeedUrl the URL of the list feed
*/
public ListBasedSpreadsheetPanel(
SpreadsheetService service, URL listFeedUrl) {
this.service = service;
this.listFeedUrl = listFeedUrl;
model = new ListTableModel();
initializeGui();
}
/**
* Refreshes the contents of the table, applying any queries the user
* specifies.
*/
private void refreshFromServer() {
try {
ListQuery query = new ListQuery(listFeedUrl);
if (!fulltextField.getText().equals("")) {
query.setFullTextQuery(fulltextField.getText());
}
if (!spreadsheetQueryField.getText().equals("")) {
query.setSpreadsheetQuery(spreadsheetQueryField.getText());
}
if (!orderbyField.getText().equals("")) {
query.setOrderBy(orderbyField.getText());
}
ListFeed feed = service.query(query, ListFeed.class);
model.resetEntries(feed.getEntries());
} catch (ServiceException e) {
SpreadsheetApiDemo.showErrorBox(e);
} catch (IOException e) {
SpreadsheetApiDemo.showErrorBox(e);
}
}
/**
* This class models one particular row in the list, tracking both its old
* contents and its new contents.
*
* This is responsible for the "commit/revert/delete" behavior that you
* see in the GUI.
*/
private class ListEntryModel {
/**
* The original entry downloaded.
* If this is an entry to add, originalEntry will be null.
*/
private ListEntry originalEntry = null;
/**
* The new contents to add.
* If this is not being edited, this will be null.
*/
private CustomElementCollection newContents = null;
/** Makes an existing row to be edited. */
public ListEntryModel(ListEntry originalEntry) {
this.originalEntry = originalEntry;
}
/** Creates a blank row to be edited. */
public ListEntryModel() {
}
/** Gets the visible contents of a particular column. */
public String getContents(String column) {
String result = null;
if (newContents != null) {
result = newContents.getValue(column);
} else if (originalEntry != null) {
result = originalEntry.getCustomElements().getValue(column);
}
if (result == null) {
result = "";
}
return result;
}
/** Gets whether this row neither has data nor is being edited. */
public boolean isBlank() {
return originalEntry == null && newContents == null;
}
/** Gets whether this row is oepn for edit. */
public boolean isBeingEdited() {
return newContents != null;
}
/** Open the row up for editing. */
public void startEdit() {
newContents = new CustomElementCollection();
if (originalEntry != null) {
newContents.replaceWithLocal(originalEntry.getCustomElements());
}
}
/** Sets the contents of a particular cell. */
public void setContents(String column, String newText) {
if (!isBeingEdited()) {
startEdit();
}
newContents.setValueLocal(column, newText);
}
/** Loses all changes. */
public void revert() {
newContents = null;
}
/** Actually adds this new entry to the spreadsheet. */
private void doAddNew() throws ServiceException, IOException {
ListEntry newEntry = new ListEntry();
newEntry.getCustomElements().replaceWithLocal(newContents);
originalEntry = service.insert(listFeedUrl, newEntry);
newContents = null;
}
/** Actually updates an existing entry on the spreadsheet,
* checking for edit conflicts. */
private void doUpdateExisting() throws ServiceException, IOException {
try {
originalEntry.getCustomElements().replaceWithLocal(newContents);
originalEntry = originalEntry.update();
newContents = null;
} catch (VersionConflictException e) {
originalEntry = originalEntry.getSelf();
TextContent content = (TextContent) originalEntry.getContent();
JOptionPane.showMessageDialog(null,
"Someone has edited the row in the meantime to:\n"
+ originalEntry.getTitle().getPlainText()
+ " (" + content.getContent().getPlainText() + ")\n"
+ "Commit again to confirm.",
"Version Conflict",
JOptionPane.WARNING_MESSAGE);
}
}
/** Commits all changes. */
public void commit() {
if (isBeingEdited()) {
boolean success = false;
try {
if (originalEntry != null) {
doUpdateExisting();
} else {
doAddNew();
}
success = true;
} catch (ServiceException e) {
SpreadsheetApiDemo.showErrorBox(e);
} catch (IOException e) {
SpreadsheetApiDemo.showErrorBox(e);
}
}
}
/** Deletes this entry from the backend, but not from the list. */
public void delete() {
if (originalEntry != null) {
try {
originalEntry.delete();
} catch (ServiceException e) {
SpreadsheetApiDemo.showErrorBox(e);
} catch (IOException e) {
SpreadsheetApiDemo.showErrorBox(e);
}
}
originalEntry = null;
newContents = null;
}
}
/**
* The Swing model for the spreadsheet.
*
* This is mostly GUI code.
*/
private class ListTableModel extends AbstractTableModel {
/** The name of each column (in the XML). */
private List<String> columnNames = new ArrayList<String>();
/** All entries of the list. */
private List<ListEntryModel> list = new ArrayList<ListEntryModel>();
/**
* Resets all the entries from a new list.
*
* This also tries to figure out what the set of valid columns is.
*/
public synchronized void resetEntries(List<ListEntry> entries) {
TreeSet<String> columnSet = new TreeSet<String>();
list.clear();
columnNames.clear();
for (ListEntry entry : entries) {
list.add(new ListEntryModel(entry));
columnSet.addAll(entry.getCustomElements().getTags());
}
// Always have an empty row to edit.
list.add(new ListEntryModel());
columnNames.add("(Edit)");
columnNames.addAll(columnSet);
fireTableStructureChanged();
fireTableDataChanged();
}
/** Writes back all modified entries to the actual spreadsheet. */
public synchronized void commitAll() {
for (ListEntryModel entryModel : list) {
entryModel.commit();
}
fireTableDataChanged();
}
/** Reverts all entries, losing all changes. */
public synchronized void revertAll() {
for (ListEntryModel entryModel : list) {
entryModel.revert();
}
fireTableDataChanged();
}
/** Delete one entry by index. */
public synchronized void deleteOne(int row) {
if (!list.get(row).isBeingEdited()) {
list.get(row).delete();
list.remove(row);
fireTableRowsDeleted(row, row);
}
}
/** Commit one entry by index. */
public synchronized void commitOne(int row) {
if (row >= 0 && row < list.size()) {
if (list.get(row).isBeingEdited()) {
list.get(row).commit();
fireTableRowsUpdated(row, row);
}
}
}
/** Revert one entry by index. */
public synchronized void revertOne(int row) {
if (list.get(row).isBeingEdited()) {
list.get(row).revert();
fireTableRowsUpdated(row, row);
}
}
/** Gets whether this is the special column. */
private boolean isSpecialColumn(int col) {
return col == 0;
}
/**
* Implements the Swing method for handling cell edits.
*/
public synchronized void setValueAt(Object value, int row, int col) {
ListEntryModel entryModel = list.get(row);
if (isSpecialColumn(col)) {
setRowEditing(row, ((Boolean) value).booleanValue());
} else {
setRowEditing(row, true);
entryModel.setContents(columnNames.get(col), value.toString());
fireTableCellUpdated(row, col);
}
}
/** Sets whether a row is being edited. */
private void setRowEditing(int row, boolean edit) {
ListEntryModel entryModel = list.get(row);
if (edit && !entryModel.isBeingEdited()) {
if (entryModel.isBlank()) {
// Always have at least two blank rows.
list.add(new ListEntryModel());
fireTableRowsInserted(list.size() - 1, list.size() - 1);
}
entryModel.startEdit();
fireTableRowsUpdated(row, row);
} else if (!edit) {
commitOne(row);
}
}
/** Tells Swing the value at a particular location. */
public synchronized Object getValueAt(int row, int col) {
ListEntryModel entryModel = list.get(row);
if (isSpecialColumn(col)) {
return Boolean.valueOf(entryModel.isBeingEdited());
} else {
return entryModel.getContents(columnNames.get(col));
}
}
/** Tells Swing whether the cell is editable. */
public synchronized boolean isCellEditable(int row, int col) {
return true;
}
/** Gets the column name by index. */
public synchronized String getColumnName(int columnIndex) {
return columnNames.get(columnIndex);
}
/** Gets the column class for editing. */
public synchronized Class<?> getColumnClass(int columnIndex) {
if (isSpecialColumn(columnIndex)) {
return Boolean.class;
} else {
return String.class;
}
}
/** Tells Swing how many rows are in this table. */
public synchronized int getRowCount() {
return list.size();
}
/** Tells Swing how many columns are in this table. */
public synchronized int getColumnCount() {
return columnNames.size();
}
}
// GUI code
public static JFrame createWindow(SpreadsheetService service,
URL listFeedUrl) {
JFrame frame = new JFrame();
frame.setSize(600, 480);
frame.getContentPane().add(new ListBasedSpreadsheetPanel(
service, listFeedUrl));
frame.setVisible(true);
frame.setTitle("List Demo - Row-based Editing");
return frame;
}
private void initializeGui() {
table = new JTable(model);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane scrollpane = new JScrollPane(table);
setLayout(new BorderLayout());
add(scrollpane, BorderLayout.CENTER);
Container southPanel = new JPanel();
southPanel.setLayout(new GridBagLayout());
deleteOneButton = new JButton("Delete");
deleteOneButton.addActionListener(new ActionHandler());
southPanel.add(deleteOneButton, getTopConstraints(0));
revertOneButton = new JButton("Revert");
revertOneButton.addActionListener(new ActionHandler());
southPanel.add(revertOneButton, getTopConstraints(1));
commitOneButton = new JButton("Commit");
commitOneButton.addActionListener(new ActionHandler());
southPanel.add(commitOneButton, getTopConstraints(2));
commitAllButton = new JButton("Commit All");
commitAllButton.addActionListener(new ActionHandler());
southPanel.add(commitAllButton, getTopConstraints(3));
refreshButton = new JButton("Refresh");
refreshButton.addActionListener(new ActionHandler());
southPanel.add(refreshButton, getTopConstraints(4));
southPanel.add(new JLabel("Full text:"), getLeftConstraints(1));
fulltextField = new JTextField();
southPanel.add(fulltextField, getRightConstraints(1));
southPanel.add(new JLabel("Structured:"), getLeftConstraints(2));
spreadsheetQueryField = new JTextField();
southPanel.add(spreadsheetQueryField, getRightConstraints(2));
southPanel.add(new JLabel("Order By:"), getLeftConstraints(3));
orderbyField = new JTextField();
southPanel.add(orderbyField, getRightConstraints(3));
add(southPanel, BorderLayout.SOUTH);
refreshFromServer();
}
private GridBagConstraints getTopConstraints(int col) {
GridBagConstraints c = new GridBagConstraints();
c.gridy = 0;
c.gridx = col;
c.fill = GridBagConstraints.HORIZONTAL;
return c;
}
private GridBagConstraints getLeftConstraints(int row) {
GridBagConstraints c = new GridBagConstraints();
c.gridy = row;
c.gridx = 0;
c.fill = GridBagConstraints.HORIZONTAL;
return c;
}
private GridBagConstraints getRightConstraints(int row) {
GridBagConstraints c = new GridBagConstraints();
c.gridy = row;
c.gridx = 1;
c.gridwidth = 4;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1.0; // take up as much space as possible
return c;
}
private class ActionHandler implements ActionListener {
public void actionPerformed(ActionEvent ae) {
if (ae.getSource() == refreshButton) {
refreshFromServer();
} else if (ae.getSource() == deleteOneButton) {
model.deleteOne(table.getSelectedRow());
} else if (ae.getSource() == revertOneButton) {
model.revertOne(table.getSelectedRow());
} else if (ae.getSource() == commitOneButton) {
model.commitOne(table.getSelectedRow());
} else if (ae.getSource() == commitAllButton) {
model.commitAll();
}
}
}
}