/* * Ext GWT - Ext for GWT * Copyright(c) 2007-2009, 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.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.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; /** * 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> * * @param <M> the model type */ public class RowEditor<M extends ModelData> extends ContentPanel implements ComponentPlugin { private Grid<M> grid; private Listener<GridEvent<M>> listener; private ClicksToEdit clicksToEdit = ClicksToEdit.ONE; private int frameWidth = 5; private boolean initialized; private ContentPanel btns; private int buttonPad = 3; private boolean editing; private int rowIndex; private Record record; private M model; private Timer monitorTimer; private boolean monitorValid = true; private boolean bound; private int monitorPoll = 200; private boolean errorSummary = true; private boolean lastValid; private ToolTip tooltip; public RowEditor() { super(); setFooter(true); setLayout(new HBoxLayout()); addStyleName("x-small-editor"); baseStyle = "x-row-editor"; } @SuppressWarnings("unchecked") public void init(Component component) { grid = (Grid<M>) component; 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) { verifyLayout(true); } else if (be.getType() == Events.BodyScroll) { positionButtons(); } else if (be.getType() == Events.Detach || be.getType() == Events.Expand || be.getType() == Events.Collapse) { stopEditing(false); } } }; if (clicksToEdit == ClicksToEdit.TWO) { grid.addListener(Events.RowDoubleClick, listener); } else { grid.addListener(Events.RowClick, listener); } grid.addListener(Events.OnKeyDown, listener); grid.addListener(Events.ColumnResize, listener); grid.addListener(Events.BodyScroll, listener); grid.addListener(Events.Attach, listener); grid.addListener(Events.Detach, listener); grid.addListener(Events.Expand, listener); grid.addListener(Events.Collapse, listener); grid.getColumnModel().addListener(Events.HiddenChange, new Listener<ColumnModelEvent>() { public void handleEvent(ColumnModelEvent be) { verifyLayout(true); } }); grid.getView().addListener(Events.Refresh, new Listener<BaseEvent>() { public void handleEvent(BaseEvent be) { stopEditing(false); } }); } @Override public void onComponentEvent(ComponentEvent ce) { super.onComponentEvent(ce); if (ce.getEventTypeInt() == KeyNav.getKeyEvent().getEventCode()) { if (ce.getKeyCode() == KeyCodes.KEY_ENTER) { stopEditing(true); } else if (ce.getKeyCode() == KeyCodes.KEY_ESCAPE) { stopEditing(false); } } } /** * 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 (editing && isDirty()) { showTooltip("You need to commit or cancel your changes"); return; } removeToolTip(); RowEditorEvent ree = new RowEditorEvent(this, rowIndex); if (!fireEvent(Events.BeforeEdit, ree)) { return; } editing = true; Element row = (Element) grid.getView().getRow(rowIndex); model = grid.getStore().getAt(rowIndex); record = getRecord(model); this.rowIndex = rowIndex; // values = new ArrayList<Object>(); 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); String dIndex = cm.getDataIndex(i); Object val = cm.getEditor(i).preProcessValue(record.get(dIndex)); f.updateOriginalValue(val); f.setValue(val); } if (!isVisible()) { show(); doLayout(); } el().setXY(El.fly(row).getXY()); verifyLayout(true); if (doFocus) { deferFocus(null); } lastValid = false; el().scrollIntoView((Element) grid.getView().getEditorParent(), false, new int[] {btns.getHeight(), 0}); } /** * Stops editing. * * @param saveChanges true to save the changes. false to ignore them. */ public void stopEditing(boolean saveChanges) { editing = false; if (!isVisible()) { return; } if (!saveChanges || !isValid()) { hide(); return; } 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)) { Field<?> f = (Field<?>) getItem(i); if (f instanceof LabelField) { continue; } String dindex = cm.getDataIndex(i); Object oldValue = record.get(dindex); Object value = cm.getEditor(i).postProcessValue(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 (hasChange && fireEvent(Events.ValidateEdit, ree)) { record.beginEdit(); for (String k : data.keySet()) { record.set(k, data.get(k)); } record.endEdit(); ree.setRecord(record); fireEvent(Events.AfterEdit, ree); } hide(); } protected void afterRender() { super.afterRender(); positionButtons(); if (monitorValid) { startMonitoring(); } btns.setWidth((getMinButtonWidth() * 2) + (frameWidth * 2) + (buttonPad * 4)); } // private protected void bindHandler() { boolean valid = isValid(); if (!valid) { lastValid = false; if (errorSummary) { showTooltip(getErrorText()); } } else if (valid && !lastValid) { removeToolTip(); lastValid = true; } btns.getItem(0).setEnabled(valid); // this.fireEvent('validation', this, valid); if (!isVisible()) { monitorTimer.cancel(); stopEditing(false); removeToolTip(); } } 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); } ColumnModel cm = this.grid.getColumnModel(); for (int i = index, 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 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 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 initFields() { ColumnModel cm = grid.getColumnModel(); for (int i = 0, len = cm.getColumnCount(); i < len; i++) { ColumnConfig c = cm.getColumn(i); CellEditor ed = c.getEditor(); if (ed == null) { ed = new CellEditor(new LabelField()); c.setEditor(ed); } Field<?> f = ed.getField(); 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)); } setNormalWidth(f); f.setMessageTarget("tooltip"); 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 onGridKey(GridEvent<M> e) { if (e.getKeyCode() == KeyCodes.KEY_ENTER && !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; model = 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); 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)); Button saveBtn = new Button("Save", new SelectionListener<ButtonEvent>() { @Override public void componentSelected(ButtonEvent ce) { stopEditing(true); } }); saveBtn.setMinWidth(getMinButtonWidth()); btns.add(saveBtn); Button cancelBtn = new Button("Cancel", new SelectionListener<ButtonEvent>() { @Override public void componentSelected(ButtonEvent ce) { stopEditing(false); } }); cancelBtn.setMinWidth(getMinButtonWidth()); btns.add(cancelBtn); btns.render(getElement("bwrap")); btns.layout(); } protected void onRowClick(GridEvent<M> e) { startEditing(e.getRowIndex(), false); deferFocus(e.getXY()); } protected void onRowDblClick(GridEvent<M> e) { startEditing(e.getRowIndex(), false); deferFocus(e.getXY()); } protected void onShow() { super.onShow(); if (monitorValid) { startMonitoring(); } } protected void positionButtons() { if (btns != null) { int h = el().getClientHeight(); GridView view = grid.getView(); int scroll = view.getScrollState().x; int width = view.mainBody.getWidth(true); int bw = btns.getWidth(true); btns.setPosition((width / 2) - (bw / 2) + scroll, h - 2); } } protected void removeToolTip() { if (tooltip != null) { tooltip.disable(); tooltip.hide(); } } protected void showTooltip(String msg) { if (tooltip == null) { ToolTipConfig config = new ToolTipConfig(); config.setAutoHide(false); config.setMouseOffset(new int[] {25, 0}); config.setTitle("Errors"); config.setAnchor("left"); tooltip = new ToolTip(this, config); tooltip.setMaxWidth(600); } ToolTipConfig config = tooltip.getToolTipConfig(); config.setText(msg); tooltip.update(config); tooltip.enable(); tooltip.show(); } 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(); } removeToolTip(); } protected void verifyLayout(boolean force) { if (isRendered() && (isVisible() || force)) { Element row = (Element) grid.getView().getRow(rowIndex); setSize(El.fly(row).getWidth(false), btns.getHeight()); 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(); MarginData md = (MarginData) ComponentHelper.getLayoutData(f); f.setWidth(cm.getColumnWidth(i) - md.getMargins().left - md.getMargins().right); } else { getItem(i).hide(); } } layout(); positionButtons(); } } private native void setNormalWidth(Field<?> f) /*-{ f.@com.extjs.gxt.ui.client.widget.form.Field::normalWidth = true; }-*/; }