/*
* Ext GWT 2.2.4 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget.grid;
import java.util.Map;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.FastMap;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.ColumnModelEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.RowEditorEvent;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ComponentHelper;
import com.extjs.gxt.ui.client.widget.ComponentManager;
import com.extjs.gxt.ui.client.widget.ComponentPlugin;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.Field;
import com.extjs.gxt.ui.client.widget.form.LabelField;
import com.extjs.gxt.ui.client.widget.form.TriggerField;
import com.extjs.gxt.ui.client.widget.grid.EditorGrid.ClicksToEdit;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayout;
import com.extjs.gxt.ui.client.widget.layout.HBoxLayoutData;
import com.extjs.gxt.ui.client.widget.layout.MarginData;
import com.extjs.gxt.ui.client.widget.layout.TableLayout;
import com.extjs.gxt.ui.client.widget.tips.ToolTip;
import com.extjs.gxt.ui.client.widget.tips.ToolTipConfig;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
/**
* This RowEditor should be used as a plugin to {@link Grid}. It displays an
* editor for all cells in a row.
*
* <dl>
* <dt><b>Events:</b></dt>
*
* <dd><b>BeforeEdit</b> : RowEditorEvent(rowEditor, rowIndex)<br>
* <div>Fires before row editing is triggered. Listeners can cancel the action
* by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>rowEditor : this</li>
* <li>rowIndex : the row index of the row about to be edited</li>
* </ul>
* </dd>
*
* <dd><b>ValidateEdit</b> : RowEditorEvent(rowEditor, rowIndex, changes)<br>
* <div>Fires right before the model is updated. Listeners can cancel the action
* by calling {@link BaseEvent#setCancelled(boolean)}.</div>
* <ul>
* <li>rowEditor : this</li>
* <li>rowIndex : the row index of the row about to be edited</li>
* <li>changes : a map of property name and new values</li>
* </ul>
* </dd>
*
* <dd><b>AfterEdit</b> : RowEditorEvent(rowEditor, rowIndex, changes)<br>
* <div>Fires after a row has been edited.</div>
* <ul>
* <li>rowEditor : this</li>
* <li>rowIndex : the row index of the row that was edited</li>
* <li>changes : a map of property name and new values</li>
* </ul>
* </dd>
* </dl>
*
* @param <M> the model type
*/
@SuppressWarnings("deprecation")
public class RowEditor<M extends ModelData> extends ContentPanel implements ComponentPlugin {
public class RowEditorMessages {
private String cancelText = GXT.MESSAGES.rowEditor_cancelText();
private String dirtyText = GXT.MESSAGES.rowEditor_dirtyText();
private String errorTipTitleText = GXT.MESSAGES.rowEditor_tipTitleText();
private String saveText = GXT.MESSAGES.rowEditor_saveText();
/**
* Returns the buttons cancel text.
*
* @return the text
*/
public String getCancelText() {
return cancelText;
}
/**
* Returns the tool tip dirty text.
*
* @return the dirtyText
*/
public String getDirtyText() {
return dirtyText;
}
/**
* Returns the error tool tip title.
*
* @return the errorTipTitleText
*/
public String getErrorTipTitleText() {
return errorTipTitleText;
}
/**
* Returns the buttons save text.
*
* @return the text
*/
public String getSaveText() {
return saveText;
}
/**
* Sets the buttons cancel text
*
* @param cancelText the cancel text
*/
public void setCancelText(String cancelText) {
this.cancelText = cancelText;
}
/**
* Sets the tool tip dirty text.
*
* @param dirtyText the dirtyText to set
*/
public void setDirtyText(String dirtyText) {
this.dirtyText = dirtyText;
}
/**
* Sets the error tool tip title.
*
* @param errorTipTitleText the errorTipTitleText to set
*/
public void setErrorTipTitleText(String errorTipTitleText) {
this.errorTipTitleText = errorTipTitleText;
}
/**
* Sets the buttons save text
*
* @param saveText the save text
*/
public void setSaveText(String saveText) {
this.saveText = saveText;
}
}
protected ContentPanel btns;
protected Grid<M> grid;
protected RowEditorMessages messages;
protected boolean renderButtons = true;
protected int rowIndex;
protected Button saveBtn, cancelBtn;
private boolean bound;
private int buttonPad = 3;
private ClicksToEdit clicksToEdit = ClicksToEdit.ONE;
private boolean editing;
private boolean errorSummary = true;
private int frameWidth = 5;
private boolean initialized;
private boolean lastValid;
private Listener<GridEvent<M>> listener;
private int monitorPoll = 200;
private Timer monitorTimer;
private boolean monitorValid = true;
private Record record;
private ToolTip tooltip;
public RowEditor() {
super();
setFooter(true);
setLayout(new HBoxLayout());
addStyleName("x-small-editor");
baseStyle = "x-row-editor";
messages = new RowEditorMessages();
}
/**
* Returns the clicks to edit.
*
* @return the clicks to edit
*/
public ClicksToEdit getClicksToEdit() {
return clicksToEdit;
}
/**
* Returns the roweditors's messages.
*
* @return the messages
*/
public RowEditorMessages getMessages() {
return messages;
}
/**
* Returns the interval in ms in that the roweditor is validated
*
* @return the interval in ms in that the roweditor is validated
*/
public int getMonitorPoll() {
return monitorPoll;
}
@SuppressWarnings("unchecked")
public void init(Component component) {
grid = (Grid<M>) component;
grid.disableTextSelection(false);
listener = new Listener<GridEvent<M>>() {
public void handleEvent(GridEvent<M> be) {
if (be.getType() == Events.RowDoubleClick) {
onRowDblClick(be);
} else if (be.getType() == Events.RowClick) {
onRowClick(be);
} else if (be.getType() == Events.OnKeyDown) {
onGridKey(be);
} else if (be.getType() == Events.ColumnResize || be.getType() == Events.Resize) {
verifyLayout(false);
} else if (be.getType() == Events.BodyScroll) {
positionButtons();
} else if (be.getType() == Events.Detach) {
stopEditing(false);
} else if (be.getType() == Events.Reconfigure && initialized) {
stopEditing(false);
removeAll();
initialized = false;
}
}
};
grid.addListener(Events.RowDoubleClick, listener);
grid.addListener(Events.Resize, listener);
grid.addListener(Events.RowClick, listener);
grid.addListener(Events.OnKeyDown, listener);
grid.addListener(Events.ColumnResize, listener);
grid.addListener(Events.BodyScroll, listener);
grid.addListener(Events.Detach, listener);
grid.addListener(Events.Reconfigure, listener);
grid.getColumnModel().addListener(Events.HiddenChange, new Listener<ColumnModelEvent>() {
public void handleEvent(ColumnModelEvent be) {
verifyLayout(false);
}
});
grid.getColumnModel().addListener(Events.ColumnMove, new Listener<ColumnModelEvent>() {
public void handleEvent(ColumnModelEvent be) {
if (initialized) {
stopEditing(false);
removeAll();
initialized = false;
}
}
});
grid.getView().addListener(Events.Refresh, new Listener<BaseEvent>() {
public void handleEvent(BaseEvent be) {
stopEditing(false);
}
});
}
/**
* Returns true of the RowEditor is active and editing.
*
* @return true if the RowEditor is active
*/
public boolean isEditing() {
return editing;
}
/**
* Returns true if a tooltip with an error summary is shown.
*
* @return true if a tooltip with an error summary is shown
*/
public boolean isErrorSummary() {
return errorSummary;
}
/**
* Returns true if this roweditor is monitored.
*
* @return true if the roweditor is monitored
*/
public boolean isMonitorValid() {
return monitorValid;
}
@Override
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
if (ce.getEventTypeInt() == KeyNav.getKeyEvent().getEventCode()) {
if (ce.getKeyCode() == KeyCodes.KEY_ENTER) {
onEnter(ce);
} else if (ce.getKeyCode() == KeyCodes.KEY_ESCAPE) {
onEscape(ce);
} else if (ce.getKeyCode() == KeyCodes.KEY_TAB) {
onTab(ce);
}
}
}
/**
* Sets the number of clicks to edit (defaults to ONE).
*
* @param clicksToEdit the clicks to edit
*/
public void setClicksToEdit(ClicksToEdit clicksToEdit) {
this.clicksToEdit = clicksToEdit;
}
/**
* True to show a tooltip with an error summary (defaults to true)
*
* @param errorSummary true to show an error summary.
*/
public void setErrorSummary(boolean errorSummary) {
this.errorSummary = errorSummary;
}
/**
* Sets the roweditors's messages.
*
* @param messages the messages
*/
public void setMessages(RowEditorMessages messages) {
this.messages = messages;
}
/**
* Sets the polling interval in ms in that the roweditor validation is done
* (defaults to 200)
*
* @param monitorPoll the polling interval in ms in that validation is done
*/
public void setMonitorPoll(int monitorPoll) {
this.monitorPoll = monitorPoll;
}
/**
* True to monitor the valid status of this roweditor (defaults to true)
*
* @param monitorValid true to monitor this roweditor
*/
public void setMonitorValid(boolean monitorValid) {
this.monitorValid = monitorValid;
}
/**
* Start editing of a specific row.
*
* @param rowIndex the index of the row to edit.
* @param doFocus true to focus the field
*/
@SuppressWarnings("unchecked")
public void startEditing(int rowIndex, boolean doFocus) {
if (disabled) {
return;
}
if (editing && isDirty()) {
showTooltip(getMessages().getDirtyText());
return;
}
hideTooltip();
M model = (M) grid.getStore().getAt(rowIndex);
Record r = getRecord(model);
RowEditorEvent ree = new RowEditorEvent(this, rowIndex);
ree.setRecord(r);
Element row = (Element) grid.getView().getRow(rowIndex);
if (row == null || model == null || !fireEvent(Events.BeforeEdit, ree)) {
return;
}
editing = true;
record = r;
this.rowIndex = rowIndex;
if (!isRendered()) {
render((Element) grid.getView().getEditorParent());
}
ComponentHelper.doAttach(this);
if (!initialized) {
initFields();
}
ColumnModel cm = grid.getColumnModel();
for (int i = 0, len = cm.getColumnCount(); i < len; i++) {
Field<Object> f = (Field<Object>) getItem(i);
if (GXT.isAriaEnabled()) {
if (i == 0 && saveBtn != null) {
saveBtn.getFocusSupport().setNextId(f.getId());
}
f.getAriaSupport().setLabel(cm.getColumnHeader(i));
}
String dIndex = cm.getDataIndex(i);
CellEditor ed = cm.getEditor(i);
Object val = ed != null ? ed.preProcessValue(record.get(dIndex)) : record.get(dIndex);
f.updateOriginalValue(val);
f.setValue(val);
}
if (cancelBtn != null) {
cancelBtn.getFocusSupport().setPreviousId(getItem(getItemCount() - 1).getId());
}
if (!isVisible()) {
show();
}
el().setXY(getPosition(row));
verifyLayout(true);
if (doFocus) {
deferFocus(null);
}
lastValid = false;
el().scrollIntoView((Element) grid.getView().getEditorParent(), false,
new int[] {renderButtons ? btns.getHeight() : 0, 0});
}
/**
* Stops editing.
*
* @param saveChanges true to save the changes. false to ignore them.
*/
public void stopEditing(boolean saveChanges) {
if (disabled || !editing) {
return;
}
editing = false;
Map<String, Object> data = new FastMap<Object>();
boolean hasChange = false;
ColumnModel cm = grid.getColumnModel();
for (int i = 0, len = cm.getColumnCount(); i < len; i++) {
if (!cm.isHidden(i)) {
Component c = getItem(i);
if (c instanceof LabelField) {
continue;
} else if (c instanceof Field<?>) {
Field<?> f = (Field<?>) c;
String dindex = cm.getDataIndex(i);
Object oldValue = record.get(dindex);
CellEditor ed = cm.getEditor(i);
Object value = ed != null ? ed.postProcessValue(f.getValue()) : f.getValue();
if ((oldValue == null && value != null) || (oldValue != null && !oldValue.equals(value))) {
data.put(dindex, value);
hasChange = true;
}
}
}
}
RowEditorEvent ree = new RowEditorEvent(this, rowIndex);
ree.setRecord(record);
ree.setChanges(data);
if (!saveChanges || !isValid()) {
fireEvent(Events.CancelEdit, ree);
} else if (hasChange && fireEvent(Events.ValidateEdit, ree)) {
record.beginEdit();
for (String k : data.keySet()) {
record.set(k, data.get(k));
}
record.endEdit();
fireEvent(Events.AfterEdit, ree);
}
hide();
}
protected void afterRender() {
super.afterRender();
positionButtons();
if (monitorValid) {
startMonitoring();
}
if (renderButtons) {
btns.setWidth((getMinButtonWidth() * 2) + (frameWidth * 2) + (buttonPad * 4));
}
}
protected void bindHandler() {
boolean valid = isValid();
if (!valid) {
lastValid = false;
if (errorSummary) {
showTooltip(getErrorText());
}
} else if (valid && !lastValid) {
hideTooltip();
lastValid = true;
}
if (saveBtn != null) {
saveBtn.setEnabled(valid);
}
if (!isVisible() && tooltip != null && tooltip.isEnabled()) {
hideTooltip();
}
}
protected void createButtons() {
btns = new ContentPanel() {
protected void createStyles(String baseStyle) {
baseStyle = "x-plain";
headerStyle = baseStyle + "-header";
headerTextStyle = baseStyle + "-header-text";
bwrapStyle = baseStyle + "-bwrap";
tbarStyle = baseStyle + "-tbar";
bodStyle = baseStyle + "-body";
bbarStyle = baseStyle + "-bbar";
footerStyle = baseStyle + "-footer";
collapseStyle = baseStyle + "-collapsed";
}
};
btns.setHeaderVisible(false);
btns.addStyleName("x-btns");
btns.setLayout(new TableLayout(2));
cancelBtn = new Button(getMessages().getCancelText(), new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
stopEditing(false);
}
});
cancelBtn.setMinWidth(getMinButtonWidth());
btns.add(cancelBtn);
saveBtn = new Button(getMessages().getSaveText(), new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
stopEditing(true);
}
});
saveBtn.setMinWidth(getMinButtonWidth());
btns.add(saveBtn);
cancelBtn.getFocusSupport().setNextId(saveBtn.getId());
saveBtn.getFocusSupport().setPreviousId(cancelBtn.getId());
btns.render(getElement("bwrap"));
btns.layout();
btns.getElement().removeAttribute("tabindex");
btns.getFocusSupport().setIgnore(true);
}
protected void deferFocus(final int colIndex) {
DeferredCommand.addCommand(new Command() {
public void execute() {
doFocus(colIndex);
}
});
}
protected void deferFocus(final Point pt) {
DeferredCommand.addCommand(new Command() {
public void execute() {
doFocus(pt);
}
});
}
@Override
protected void doAttachChildren() {
super.doAttachChildren();
ComponentHelper.doAttach(btns);
}
@Override
protected void doDetachChildren() {
super.doDetachChildren();
ComponentHelper.doDetach(btns);
}
protected void doFocus(Point pt) {
if (isVisible()) {
int index = 0;
if (pt != null) {
index = getTargetColumnIndex(pt);
}
doFocus(index);
}
}
protected void doFocus(int colIndex) {
if (isVisible()) {
ColumnModel cm = this.grid.getColumnModel();
for (int i = colIndex, len = cm.getColumnCount(); i < len; i++) {
ColumnConfig c = cm.getColumn(i);
if (!c.isHidden() && c.getEditor() != null) {
c.getEditor().getField().focus();
break;
}
}
}
}
protected void ensureVisible(CellEditor editor) {
if (isVisible()) {
grid.getView().ensureVisible(this.rowIndex, indexOf(editor), true);
}
}
protected Component findField(Element elem) {
El e = El.fly(elem).findParent(".x-row-editor-field", 3);
if (e != null) {
return ComponentManager.get().get(e.getId());
}
return null;
}
protected String getErrorText() {
StringBuffer sb = new StringBuffer();
sb.append("<ul>");
for (int i = 0; i < getItemCount(); i++) {
Field<?> f = (Field<?>) getItem(i);
if (!f.isValid(true)) {
sb.append("<li><b>");
sb.append(grid.getColumnModel().getColumn(i).getHeader());
sb.append("</b>: ");
sb.append(f.getErrorMessage());
sb.append("</li>");
}
}
sb.append("</ul>");
return sb.toString();
}
protected Point getPosition(Element row) {
return El.fly(row).getXY();
}
protected Record getRecord(M model) {
return grid.getStore().getRecord(model);
}
protected int getTargetColumnIndex(Point pt) {
int x = pt.x;
int match = -1;
for (int i = 0; i < grid.getColumnModel().getColumnCount(); i++) {
ColumnConfig c = grid.getColumnModel().getColumn(i);
if (!c.isHidden()) {
if (El.fly(grid.getView().getHeaderCell(i)).getRegion().right >= x) {
match = i;
break;
}
}
}
return match;
}
protected void hideTooltip() {
if (tooltip != null) {
tooltip.hide();
tooltip.disable();
}
}
protected void initFields() {
ColumnModel cm = grid.getColumnModel();
for (int i = 0, len = cm.getColumnCount(); i < len; i++) {
ColumnConfig c = cm.getColumn(i);
CellEditor ed = c.getEditor();
Field<?> f = ed != null ? ed.getField() : new LabelField();
if (f instanceof TriggerField<?>) {
((TriggerField<? extends Object>) f).setMonitorTab(true);
}
f.setWidth(cm.getColumnWidth(i));
HBoxLayoutData ld = new HBoxLayoutData();
if (i == 0) {
ld.setMargins(new Margins(0, 1, 2, 1));
} else if (i == len - 1) {
ld.setMargins(new Margins(0, 0, 2, 1));
} else {
ld.setMargins(new Margins(0, 1, 2, 2));
}
f.setMessageTarget("tooltip");
f.addStyleName("x-row-editor-field");
// needed because we remove it from the celleditor
clearParent(f);
insert(f, i, ld);
}
initialized = true;
}
@SuppressWarnings("unchecked")
protected boolean isDirty() {
for (Component f : getItems()) {
if (((Field<Object>) f).isDirty()) {
return true;
}
}
return false;
}
protected boolean isValid() {
boolean valid = true;
for (Component c : getItems()) {
Field<?> f = (Field<?>) c;
if (!f.isValid(true)) {
return false;
}
}
return valid;
}
protected void onEnter(ComponentEvent ce) {
stopEditing(true);
}
protected void onEscape(ComponentEvent ce) {
stopEditing(false);
}
protected void onGridKey(GridEvent<M> e) {
int kc = e.getKeyCode();
if ((kc == KeyCodes.KEY_ENTER || (kc == 113 && GXT.isWindows)) && !isVisible()) {
M r = grid.getSelectionModel().getSelectedItem();
if (r != null) {
int index = this.grid.store.indexOf(r);
startEditing(index, true);
e.cancelBubble();
}
}
}
protected void onHide() {
super.onHide();
stopMonitoring();
grid.getView().focusRow(rowIndex);
record = null;
ComponentHelper.doDetach(this);
}
@Override
protected void onRender(Element target, int index) {
super.onRender(target, index);
el().makePositionable(true);
sinkEvents(KeyNav.getKeyEvent().getEventCode());
swallowEvent(Events.OnKeyDown, el().dom, false);
swallowEvent(Events.OnKeyUp, el().dom, false);
swallowEvent(Events.OnKeyPress, el().dom, false);
if (renderButtons) {
createButtons();
ComponentHelper.setParent(this, btns);
}
}
protected void onRowClick(GridEvent<M> e) {
if (clicksToEdit != ClicksToEdit.TWO) {
startEditing(e.getRowIndex(), false);
deferFocus(e.getColIndex());
}
}
protected void onRowDblClick(GridEvent<M> e) {
if (clicksToEdit == ClicksToEdit.TWO) {
startEditing(e.getRowIndex(), false);
deferFocus(e.getColIndex());
}
}
protected void onShow() {
super.onShow();
if (monitorValid) {
startMonitoring();
}
}
protected void onTab(ComponentEvent ce) {
Element target = ce.getTarget();
Component c = findField(target);
if (saveBtn != null && c != null && ce.isShiftKey() && indexOf(c) == 0) {
ce.stopEvent();
saveBtn.focus();
return;
}
}
protected void positionButtons() {
if (btns != null) {
int h = el().getClientHeight();
GridView view = grid.getView();
int scroll = view.getScrollState().x;
int mainBodyWidth = view.scroller.getWidth(true);
int columnWidth = view.getTotalWidth();
int width = columnWidth < mainBodyWidth ? columnWidth : mainBodyWidth;
int bw = btns.getWidth(true);
this.btns.setPosition((width / 2) - (bw / 2) + scroll, h - 2);
}
}
protected void showTooltip(String msg) {
if (tooltip == null) {
ToolTipConfig config = new ToolTipConfig();
config.setAutoHide(false);
config.setMouseOffset(new int[] {0, 0});
config.setTitle(getMessages().getErrorTipTitleText());
config.setAnchor("left");
tooltip = new ToolTip(this, config);
tooltip.setMaxWidth(600);
}
ToolTipConfig config = tooltip.getToolTipConfig();
config.setText(msg);
tooltip.update(config);
tooltip.enable();
if (!tooltip.isAttached()) {
tooltip.show();
tooltip.el().updateZIndex(0);
}
}
protected void startMonitoring() {
if (!bound && monitorValid) {
bound = true;
if (monitorTimer == null) {
monitorTimer = new Timer() {
@Override
public void run() {
RowEditor.this.bindHandler();
}
};
}
monitorTimer.scheduleRepeating(monitorPoll);
}
}
protected void stopMonitoring() {
bound = false;
if (monitorTimer != null) {
monitorTimer.cancel();
}
hideToolTip();
}
protected void verifyLayout(boolean force) {
if (initialized && (isVisible() || force)) {
Element row = (Element) grid.getView().getRow(rowIndex);
setSize(El.fly(row).getWidth(false), renderButtons ? btns.getHeight() : 0);
syncSize();
ColumnModel cm = grid.getColumnModel();
for (int i = 0, len = cm.getColumnCount(); i < len; i++) {
if (!cm.isHidden(i)) {
Field<?> f = (Field<?>) getItem(i);
f.show();
f.getElement().setAttribute("gxt-dindex", "" + cm.getDataIndex(i));
MarginData md = (MarginData) ComponentHelper.getLayoutData(f);
f.setWidth(cm.getColumnWidth(i) - md.getMargins().left - md.getMargins().right);
} else {
getItem(i).hide();
}
}
layout(true);
positionButtons();
}
}
private native void clearParent(Widget parent) /*-{
parent.@com.google.gwt.user.client.ui.Widget::parent=null;
}-*/;
}