/******************************************************************************* * Copyright (c) 2012, 2016 Original authors and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Original authors and others - initial API and implementation * Dirk Fauth <dirk.fauth@googlemail.com> - Bug 469486 ******************************************************************************/ package org.eclipse.nebula.widgets.nattable.edit.editor; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.fieldassist.ContentProposalAdapter; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.fieldassist.FieldDecoration; import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalListener; import org.eclipse.jface.fieldassist.IContentProposalListener2; import org.eclipse.jface.fieldassist.IContentProposalProvider; import org.eclipse.jface.fieldassist.IControlContentAdapter; import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes; import org.eclipse.nebula.widgets.nattable.edit.config.RenderErrorHandling; import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum; import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes; import org.eclipse.nebula.widgets.nattable.style.DisplayMode; import org.eclipse.nebula.widgets.nattable.style.HorizontalAlignmentEnum; import org.eclipse.nebula.widgets.nattable.style.IStyle; import org.eclipse.nebula.widgets.nattable.widget.EditModeEnum; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Text; /** * {@link ICellEditor} implementation that wraps a SWT {@link Text} control to * support text editing. This is also the default editor in NatTable if you * didn't configure something else. */ public class TextCellEditor extends AbstractCellEditor { /** * The Text control which is the editor wrapped by this TextCellEditor. */ private Text text = null; /** * Flag to configure if the wrapped text editor control is editable or not. */ private boolean editable = true; /** * Flag to configure whether the editor should commit and move the selection * in the corresponding way if the up or down key is pressed. */ private final boolean commitOnUpDown; /** * Flag to configure whether the editor should commit and move the selection * in the corresponding way if the left or right key is pressed on the * according content edge. */ private final boolean commitOnLeftRight; /** * Flag to configure whether the selection should move after a value was * committed after pressing enter. */ private final boolean moveSelectionOnEnter; /** * The selection mode that should be used on activating the wrapped text * control. By default the behaviour is to set the selection at the end of * the containing text if the text editor control is activated with an * initial value. If it is activated only specifying the original canonical * value, the default behaviour is to select the whole text contained in the * text editor control. * * <p> * You can override this default behaviour by setting an * {@link EditorSelectionEnum} explicitly. With this you are able e.g. to * set the selection at the beginning of the contained text, so writing in * the text control will result in prefixing. * * <p> * Note that on overriding the behaviour, you override both activation * cases. */ private EditorSelectionEnum selectionMode; /** * The {@link ControlDecorationProvider} responsible for adding a * {@link ControlDecoration} to the wrapped editor control. Can be * configured via convenience methods of this TextCellEditor. */ protected final ControlDecorationProvider decorationProvider = new ControlDecorationProvider(); /** * The {@link IEditErrorHandler} that is used for showing conversion errors * on typing into this editor. By default this is the * {@link RenderErrorHandling} which will render the content in the editor * red to indicate a conversion error. */ private IEditErrorHandler inputConversionErrorHandler = new RenderErrorHandling(this.decorationProvider); /** * The {@link IEditErrorHandler} that is used for showing validation errors * on typing into this editor. By default this is the * {@link RenderErrorHandling} which will render the content in the editor * red to indicate a validation error. */ private IEditErrorHandler inputValidationErrorHandler = new RenderErrorHandling(this.decorationProvider); /** * Flag to determine whether this editor should try to commit and close on * pressing the enter key. The default is of course <code>true</code>, but * for a multi line text editor, the enter key should be treated as * inserting a new line instead of committing. */ protected boolean commitOnEnter = true; /** * @see ContentProposalAdapter#ContentProposalAdapter(Control, * IControlContentAdapter, IContentProposalProvider, KeyStroke, char[]) * @since 1.4 */ protected IControlContentAdapter controlContentAdapter; /** * @see ContentProposalAdapter#ContentProposalAdapter(Control, * IControlContentAdapter, IContentProposalProvider, KeyStroke, char[]) * @since 1.4 */ protected IContentProposalProvider proposalProvider; /** * @see ContentProposalAdapter#ContentProposalAdapter(Control, * IControlContentAdapter, IContentProposalProvider, KeyStroke, char[]) * @since 1.4 */ protected KeyStroke keyStroke; /** * @see ContentProposalAdapter#ContentProposalAdapter(Control, * IControlContentAdapter, IContentProposalProvider, KeyStroke, char[]) * @since 1.4 */ protected char[] autoActivationCharacters; /** * Creates the default TextCellEditor that does not commit on pressing the * up/down arrow keys and will not move the selection on committing a value * by pressing enter. */ public TextCellEditor() { this(false); } /** * Creates a TextCellEditor that will not move the selection on committing a * value by pressing enter. * * @param commitOnUpDown * Flag to configure whether the editor should commit and move * the selection in the corresponding way if the up or down key * is pressed. */ public TextCellEditor(boolean commitOnUpDown) { this(commitOnUpDown, false); } /** * Creates a TextCellEditor that will not move the selection on pressing the * left or right arrow keys on the according edges. * * @param commitOnUpDown * Flag to configure whether the editor should commit and move * the selection in the corresponding way if the up or down key * is pressed. * @param moveSelectionOnEnter * Flag to configure whether the selection should move after a * value was committed after pressing enter. */ public TextCellEditor(boolean commitOnUpDown, boolean moveSelectionOnEnter) { this(commitOnUpDown, moveSelectionOnEnter, false); } /** * Creates a TextCellEditor. * * @param commitOnUpDown * Flag to configure whether the editor should commit and move * the selection in the corresponding way if the up or down key * is pressed. * @param moveSelectionOnEnter * Flag to configure whether the selection should move after a * value was committed after pressing enter. * @param commitOnLeftRight * Flag to configure whether the editor should commit and move * the selection in the corresponding way if the left or right * key is pressed on the according content edge. * @since 1.4 */ public TextCellEditor(boolean commitOnUpDown, boolean moveSelectionOnEnter, boolean commitOnLeftRight) { this.commitOnUpDown = commitOnUpDown; this.moveSelectionOnEnter = moveSelectionOnEnter; this.commitOnLeftRight = commitOnLeftRight; } @Override protected Control activateCell(final Composite parent, Object originalCanonicalValue) { this.text = createEditorControl(parent); // If the originalCanonicalValue is a Character it is possible the // editor is activated by keypress if (originalCanonicalValue instanceof Character) { this.text.setText(originalCanonicalValue.toString()); selectText(this.selectionMode != null ? this.selectionMode : EditorSelectionEnum.END); } // if there is no initial value, handle the original canonical value to // transfer it to the text control else { setCanonicalValue(originalCanonicalValue); selectText(this.selectionMode != null ? this.selectionMode : EditorSelectionEnum.ALL); } if (!isEditable()) { this.text.setEditable(false); } // show an error decoration if this is enabled this.decorationProvider.createErrorDecorationIfRequired(this.text); // if the input error handlers are of type RenderErrorHandler (default) // than we also check for a possible configured error styling in the // configuration // Note: this is currently only implemented in here, as the // TextCellEditor is the only editor that supports just in time // conversion/validation if (this.inputConversionErrorHandler instanceof RenderErrorHandling) { IStyle conversionErrorStyle = this.configRegistry.getConfigAttribute( EditConfigAttributes.CONVERSION_ERROR_STYLE, DisplayMode.EDIT, this.labelStack.getLabels()); ((RenderErrorHandling) this.inputConversionErrorHandler).setErrorStyle(conversionErrorStyle); } if (this.inputValidationErrorHandler instanceof RenderErrorHandling) { IStyle validationErrorStyle = this.configRegistry.getConfigAttribute( EditConfigAttributes.VALIDATION_ERROR_STYLE, DisplayMode.EDIT, this.labelStack.getLabels()); ((RenderErrorHandling) this.inputValidationErrorHandler).setErrorStyle(validationErrorStyle); } // if a IControlContentAdapter is registered, create and register a // ContentProposalAdapter if (this.controlContentAdapter != null) { configureContentProposalAdapter( new ContentProposalAdapter( this.text, this.controlContentAdapter, this.proposalProvider, this.keyStroke, this.autoActivationCharacters)); } this.text.forceFocus(); return this.text; } @Override public String getEditorValue() { return this.text.getText(); } @Override public void setEditorValue(Object value) { this.text.setText(value != null && value.toString().length() > 0 ? value.toString() : ""); //$NON-NLS-1$ } @Override public Text getEditorControl() { return this.text; } @Override public Text createEditorControl(Composite parent) { int style = HorizontalAlignmentEnum.getSWTStyle(this.cellStyle); if (this.editMode == EditModeEnum.DIALOG) { style = style | SWT.BORDER; } return createEditorControl(parent, style); } /** * Creates the editor control that is wrapped by this ICellEditor. Will use * the style configurations in ConfigRegistry for styling the control. * * @param parent * The Composite that will be the parent of the new editor * control. Can not be <code>null</code> * @param style * The SWT style of the text control to create. * @return The created editor control that is wrapped by this ICellEditor. */ protected Text createEditorControl(final Composite parent, int style) { // create the Text control based on the specified style final Text textControl = new Text(parent, style); // set style information configured in the associated cell style textControl.setBackground(this.cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR)); textControl.setForeground(this.cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR)); textControl.setFont(this.cellStyle.getAttributeValue(CellStyleAttributes.FONT)); textControl.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_IBEAM)); // add a key listener that will commit or close the editor for special // key strokes and executes conversion/validation on input to the editor textControl.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { if (TextCellEditor.this.commitOnEnter && (event.keyCode == SWT.CR || event.keyCode == SWT.KEYPAD_CR)) { boolean commit = (event.stateMask == SWT.MOD3) ? false : true; MoveDirectionEnum move = MoveDirectionEnum.NONE; if (TextCellEditor.this.moveSelectionOnEnter && TextCellEditor.this.editMode == EditModeEnum.INLINE) { if (event.stateMask == 0) { move = MoveDirectionEnum.DOWN; } else if (event.stateMask == SWT.MOD2) { move = MoveDirectionEnum.UP; } } if (commit) { commit(move); } if (TextCellEditor.this.editMode == EditModeEnum.DIALOG) { parent.forceFocus(); } } else if (event.keyCode == SWT.ESC && event.stateMask == 0) { close(); } else if ((TextCellEditor.this.commitOnUpDown || TextCellEditor.this.commitOnLeftRight) && TextCellEditor.this.editMode == EditModeEnum.INLINE) { Text control = (Text) event.widget; if (TextCellEditor.this.commitOnUpDown && event.keyCode == SWT.ARROW_UP) { commit(MoveDirectionEnum.UP); } else if (TextCellEditor.this.commitOnUpDown && event.keyCode == SWT.ARROW_DOWN) { commit(MoveDirectionEnum.DOWN); } else if (TextCellEditor.this.commitOnLeftRight && control.getSelectionCount() == 0 && event.keyCode == SWT.ARROW_LEFT && control.getCaretPosition() == 0) { commit(MoveDirectionEnum.LEFT); } else if (TextCellEditor.this.commitOnLeftRight && control.getSelectionCount() == 0 && event.keyCode == SWT.ARROW_RIGHT && control.getCaretPosition() == control.getCharCount()) { commit(MoveDirectionEnum.RIGHT); } } } @Override public void keyReleased(KeyEvent e) { try { // always do the conversion Object canonicalValue = getCanonicalValue(TextCellEditor.this.inputConversionErrorHandler); // and always do the validation, even if for committing the // validation should be skipped, on editing // a validation failure should be made visible // otherwise there would be no need for validation! validateCanonicalValue(canonicalValue, TextCellEditor.this.inputValidationErrorHandler); } catch (Exception ex) { // do nothing as exceptions caused by conversion or // validation are handled already we just need this catch // block for stopping the process if conversion failed with // an exception } } }); return textControl; } @Override public void close() { // ensure to reset the error handlers in case this editor was closed // rendering invalid this is necessary because if the editor is closed // rendering invalid, opening the editor again inserting an invalid // value again, it is not rendered invalid because of a wrong state in // the internal error handlers if (this.inputConversionErrorHandler != null) { this.inputConversionErrorHandler.removeError(this); } if (this.inputValidationErrorHandler != null) { this.inputValidationErrorHandler.removeError(this); } super.close(); this.decorationProvider.dispose(); } /** * @return <code>true</code> if the wrapped Text control is editable, * <code>false</code> if not. */ public boolean isEditable() { return this.editable; } /** * * @param editable * <code>true</code> if the wrapped Text control should be * editable, <code>false</code> if not. */ public void setEditable(boolean editable) { this.editable = editable; } /** * Returns the current configured selection mode that is used on activating * the wrapped text editor control. By default this is <code>null</code> * which causes the following default behaviour. If the text editor control * is activated with an initial value then the selection is set at the end * of the containing text. If it is activated only specifying the original * canonical value, the default behaviour is to select the whole text * contained in the text editor control. * * @return The current configured selection mode, <code>null</code> for * default behaviour. */ public final EditorSelectionEnum getSelectionMode() { return this.selectionMode; } /** * Set the selection mode that should be used on the content of the wrapped * text editor control when it gets activated. By setting a value explicitly * you configure the selection mode for both cases, activating the wrapped * text editor control with and without an initial value. Setting this value * to <code>null</code> will reactivate the default behaviour like described * here {@link TextCellEditor#getSelectionMode()}. * * @param selectionMode * The selection mode that should be used on the content of the * wrapped text editor control when it gets activated. */ public final void setSelectionMode(EditorSelectionEnum selectionMode) { this.selectionMode = selectionMode; } /** * Will set the selection to the wrapped text control regarding the * configured {@link EditorSelectionEnum}. * * <p> * This method is called * * @see Text#setSelection(int, int) */ private void selectText(EditorSelectionEnum selectionMode) { int textLength = this.text.getText().length(); if (textLength > 0) { if (selectionMode == EditorSelectionEnum.ALL) { this.text.setSelection(0, textLength); } else if (selectionMode == EditorSelectionEnum.END) { this.text.setSelection(textLength, textLength); } else if (selectionMode == EditorSelectionEnum.START) { this.text.setSelection(0); } } } /** * @return The {@link ControlDecorationProvider} responsible for adding a * {@link ControlDecoration} to the wrapped editor control. */ public ControlDecorationProvider getDecorationProvider() { return this.decorationProvider; } /** * Enables/disables the error decoration for the wrapped text control. * * @param enabled * <code>true</code> if an error decoration should be added to * the wrapped text control, <code>false</code> if not. */ public void setErrorDecorationEnabled(boolean enabled) { this.decorationProvider.setErrorDecorationEnabled(enabled); } /** * Set the error description text that will be shown in the decoration * hover. * * @param errorText * The text to be shown as a description for the decoration, or * <code>null</code> if there should be no description. * * @see ControlDecoration#setDescriptionText(String) */ public void setErrorDecorationText(String errorText) { this.decorationProvider.setErrorDecorationText(errorText); } /** * Force the error decoration hover to show immediately. * * @param customErrorText * The text to show in the hover popup. * * @see ControlDecoration#show() * @see ControlDecoration#showHoverText(String) */ public void showErrorDecorationHover(String customErrorText) { this.decorationProvider.showErrorDecorationHover(customErrorText); } /** * Set the id of the {@link FieldDecoration} to be used by the local * {@link ControlDecorationProvider}. * * @param fieldDecorationId * The String to determine the {@link FieldDecoration} to use by * the {@link ControlDecoration} that is provided by this * {@link ControlDecorationProvider}. * * @see FieldDecorationRegistry#getFieldDecoration(String) */ public void setFieldDecorationId(String fieldDecorationId) { this.decorationProvider.setFieldDecorationId(fieldDecorationId); } /** * Set the position of the control decoration relative to the control. It * should include style bits describing both the vertical and horizontal * orientation. * * @param decorationPositionOverride * bit-wise or of position constants (<code>SWT.TOP</code>, * <code>SWT.BOTTOM</code>, <code>SWT.LEFT</code>, * <code>SWT.RIGHT</code>, and <code>SWT.CENTER</code>). * * @see ControlDecoration#ControlDecoration(Control, int) */ public void setDecorationPositionOverride(int decorationPositionOverride) { this.decorationProvider.setDecorationPositionOverride(decorationPositionOverride); } /** * @return The {@link IEditErrorHandler} that is used for showing conversion * errors on typing into this editor. By default this is the * {@link RenderErrorHandling} which will render the content in the * editor red to indicate a conversion error. */ public IEditErrorHandler getInputConversionErrorHandler() { return this.inputConversionErrorHandler; } /** * @param inputConversionErrorHandler * The {@link IEditErrorHandler} that is should be used for * showing conversion errors on typing into this editor. */ public void setInputConversionErrorHandler(IEditErrorHandler inputConversionErrorHandler) { this.inputConversionErrorHandler = inputConversionErrorHandler; } /** * @return The {@link IEditErrorHandler} that is used for showing validation * errors on typing into this editor. By default this is the * {@link RenderErrorHandling} which will render the content in the * editor red to indicate a validation error. */ public IEditErrorHandler getInputValidationErrorHandler() { return this.inputValidationErrorHandler; } /** * @param inputValidationErrorHandler * The {@link IEditErrorHandler} that is should used for showing * validation errors on typing into this editor. */ public void setInputValidationErrorHandler(IEditErrorHandler inputValidationErrorHandler) { this.inputValidationErrorHandler = inputValidationErrorHandler; } /** * Configure the parameters necessary to create the content proposal adapter * on opening an editor. * * @param controlContentAdapter * the <code>IControlContentAdapter</code> used to obtain and * update the control's contents as proposals are accepted. May * not be <code>null</code>. * @param proposalProvider * the <code>IContentProposalProvider</code> used to obtain * content proposals for this control, or <code>null</code> if no * content proposal is available. * @param keyStroke * the keystroke that will invoke the content proposal popup. If * this value is <code>null</code>, then proposals will be * activated automatically when any of the auto activation * characters are typed. * @param autoActivationCharacters * An array of characters that trigger auto-activation of content * proposal. If specified, these characters will trigger * auto-activation of the proposal popup, regardless of whether * an explicit invocation keyStroke was specified. If this * parameter is <code>null</code>, then only a specified * keyStroke will invoke content proposal. If this parameter is * <code>null</code> and the keyStroke parameter is * <code>null</code>, then all alphanumeric characters will * auto-activate content proposal. * * @see ContentProposalAdapter * @since 1.4 */ public void enableContentProposal( IControlContentAdapter controlContentAdapter, IContentProposalProvider proposalProvider, KeyStroke keyStroke, char[] autoActivationCharacters) { this.controlContentAdapter = controlContentAdapter; this.proposalProvider = proposalProvider; this.keyStroke = keyStroke; this.autoActivationCharacters = autoActivationCharacters; } /** * Adds the listeners necessary for interaction between the control of this * TextCellEditor and the ContentProposalAdapter. * * @param contentProposalAdapter * The {@link ContentProposalAdapter} that should be used to add * content proposal abilities to this {@link TextCellEditor}. * @since 1.4 */ protected void configureContentProposalAdapter(final ContentProposalAdapter contentProposalAdapter) { // add the necessary listeners to support the interaction between the // content proposal and this text editor contentProposalAdapter.addContentProposalListener(new IContentProposalListener() { @Override public void proposalAccepted(IContentProposal proposal) { commit(MoveDirectionEnum.NONE); } }); contentProposalAdapter.addContentProposalListener(new IContentProposalListener2() { @Override public void proposalPopupClosed(ContentProposalAdapter adapter) { if (TextCellEditor.this.focusListener instanceof InlineFocusListener) { ((InlineFocusListener) TextCellEditor.this.focusListener).handleFocusChanges = true; } } @Override public void proposalPopupOpened(ContentProposalAdapter adapter) { if (TextCellEditor.this.focusListener instanceof InlineFocusListener) { ((InlineFocusListener) TextCellEditor.this.focusListener).handleFocusChanges = false; } // set the focus to the popup so on enabling via keystroke the // selection via keyboard is immediately possible contentProposalAdapter.setProposalPopupFocus(); } }); } }