/* * Copyright 2016 Nokia Solutions and Networks * Licensed under the Apache License, Version 2.0, * see license.txt file for details. */ package org.robotframework.red.nattable.edit; import java.util.List; import java.util.Optional; import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry; import org.eclipse.nebula.widgets.nattable.edit.editor.TextCellEditor; import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum; import org.eclipse.nebula.widgets.nattable.selection.command.SelectCellCommand; import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes; 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; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.contexts.IContextActivation; import org.eclipse.ui.contexts.IContextService; import org.robotframework.ide.eclipse.main.plugin.RedPlugin; import org.robotframework.ide.eclipse.main.plugin.RedPreferences.CellCommitBehavior; import org.robotframework.red.jface.assist.AssistantContext; import org.robotframework.red.jface.assist.RedContentProposal; import org.robotframework.red.jface.assist.RedContentProposalAdapter; import org.robotframework.red.jface.assist.RedContentProposalAdapter.RedContentProposalListener; import org.robotframework.red.jface.assist.RedContentProposalProvider; import org.robotframework.red.nattable.edit.AssistanceSupport.NatTableAssistantContext; import org.robotframework.red.swt.SwtThread; import com.google.common.annotations.VisibleForTesting; /** * Modified version of {@link org.eclipse.nebula.widgets.nattable.edit.editor.TextCellEditor} which * will move left/right after commits and validate entries asynchronously. * * @author Michal Anglart */ public class RedTextCellEditor extends TextCellEditor { public static final String DETAILS_EDITING_CONTEXT_ID = "org.robotframework.ide.eclipse.details.context"; private final int selectionStartShift; private final int selectionEndShift; private final CellEditorValueValidationJobScheduler<String> validationJobScheduler; private final AssistanceSupport support; private final boolean wrapCellContent; private IContextActivation contextActivation; public RedTextCellEditor(final boolean wrapCellContent) { this(0, 0, new DefaultRedCellEditorValueValidator(), null, wrapCellContent); } public RedTextCellEditor(final RedContentProposalProvider proposalProvider, final boolean wrapCellContent) { this(0, 0, new DefaultRedCellEditorValueValidator(), proposalProvider, wrapCellContent); } public RedTextCellEditor(final CellEditorValueValidator<String> validator, final boolean wrapCellContent) { this(0, 0, validator, null, wrapCellContent); } public RedTextCellEditor(final int selectionStartShift, final int selectionEndShift, final boolean wrapCellContent) { this(selectionStartShift, selectionEndShift, new DefaultRedCellEditorValueValidator(), null, wrapCellContent); } public RedTextCellEditor(final int selectionStartShift, final int selectionEndShift, final CellEditorValueValidator<String> validator, final boolean wrapCellContent) { this(selectionStartShift, selectionEndShift, validator, null, wrapCellContent); } public RedTextCellEditor(final int selectionStartShift, final int selectionEndShift, final CellEditorValueValidator<String> validator, final RedContentProposalProvider proposalProvider, final boolean wrapCellContent) { super(true, true); this.selectionStartShift = selectionStartShift; this.selectionEndShift = selectionEndShift; this.wrapCellContent = wrapCellContent; this.support = new AssistanceSupport(proposalProvider); this.validationJobScheduler = new CellEditorValueValidationJobScheduler<>(validator); } @Override public boolean supportMultiEdit(final IConfigRegistry configRegistry, final List<String> configLabels) { return false; } @Override protected Text createEditorControl(final Composite parent, final int style) { final int finalStyle = wrapCellContent ? style | SWT.WRAP | SWT.MULTI : style; final Text textControl = new Text(parent, finalStyle); 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)); textControl.addKeyListener(new TextKeyListener(parent)); validationJobScheduler.armRevalidationOn(textControl); return textControl; } @Override protected Control activateCell(final Composite parent, final Object originalCanonicalValue) { // workaround: switching off the focus listener during activation; this was causing // some incosistent state where newly created cell editor was not responsive ((InlineFocusListener) focusListener).handleFocusChanges = false; final Text text = (Text) super.activateCell(parent, originalCanonicalValue); final RedContentProposalListener assistListener = new ContentProposalsListener( (InlineFocusListener) focusListener); final AssistantContext context = new NatTableAssistantContext(getColumnIndex(), getRowIndex()); support.install(text, context, Optional.of(assistListener)); parent.redraw(); if ((selectionStartShift > 0 || selectionEndShift > 0) && !text.isDisposed()) { if (text.getText().length() >= selectionStartShift + selectionEndShift) { text.setSelection(selectionStartShift, text.getText().length() - selectionEndShift); } } ((InlineFocusListener) focusListener).handleFocusChanges = true; final IContextService service = PlatformUI.getWorkbench().getService(IContextService.class); contextActivation = service.activateContext(RedPlugin.DETAILS_EDITING_CONTEXT_ID); return text; } @Override public boolean commit(final MoveDirectionEnum direction, final boolean closeAfterCommit, final boolean skipValidation) { if (validationJobScheduler.canCloseCellEditor()) { removeEditorControlListeners(); final boolean commited = super.commit(direction, closeAfterCommit, skipValidation); if (direction == MoveDirectionEnum.NONE) { layerCell.getLayer().doCommand(new SelectCellCommand(layerCell.getLayer(), layerCell.getColumnPosition(), layerCell.getRowPosition(), false, false)); } return commited; } else { return false; } } @Override public void close() { super.close(); final IContextService service = PlatformUI.getWorkbench().getService(IContextService.class); service.deactivateContext(contextActivation); } @VisibleForTesting public CellEditorValueValidationJobScheduler<String> getValidationJobScheduler() { return this.validationJobScheduler; } private class TextKeyListener extends KeyAdapter { private final Composite parent; private TextKeyListener(final Composite parent) { this.parent = parent; } @Override public void keyPressed(final KeyEvent event) { if (support.areContentProposalsShown()) { return; } if (commitOnEnter && (event.keyCode == SWT.CR || event.keyCode == SWT.KEYPAD_CR)) { final boolean commit = event.stateMask != SWT.ALT; if (commit) { final boolean commited = commit(getMoveDirection(event)); if (!commited && wrapCellContent) { // when there is multiline Text control we don't want to // have new lines there, so cannot deliver this event to // control event.doit = false; } } if (RedTextCellEditor.this.editMode == EditModeEnum.DIALOG) { parent.forceFocus(); } } else if (event.keyCode == SWT.ESC && event.stateMask == 0) { close(); } else if (RedTextCellEditor.this.editMode == EditModeEnum.INLINE && !wrapCellContent) { if (event.keyCode == SWT.ARROW_UP) { commit(MoveDirectionEnum.UP); } else if (event.keyCode == SWT.ARROW_DOWN) { commit(MoveDirectionEnum.DOWN); } } } private MoveDirectionEnum getMoveDirection(final KeyEvent event) { if (RedPlugin.getDefault() .getPreferences() .getCellCommitBehavior() == CellCommitBehavior.STAY_IN_SAME_CELL) { return MoveDirectionEnum.NONE; } if (RedTextCellEditor.this.editMode == EditModeEnum.INLINE) { if (event.stateMask == 0) { return MoveDirectionEnum.RIGHT; } else if (event.stateMask == SWT.SHIFT) { return MoveDirectionEnum.LEFT; } } return MoveDirectionEnum.NONE; } @Override public void keyReleased(final KeyEvent e) { try { final Object canonicalValue = getCanonicalValue(getInputConversionErrorHandler()); validateCanonicalValue(canonicalValue, getInputValidationErrorHandler()); } catch (final Exception ex) { // do nothing } } } private class ContentProposalsListener implements RedContentProposalListener { private final InlineFocusListener focusListener; public ContentProposalsListener(final InlineFocusListener focusListener) { this.focusListener = focusListener; } @Override public void proposalPopupOpened(final RedContentProposalAdapter adapter) { // under GTK2 when user double-clicks proposal the focus is lost which // results in editor closing focusListener.handleFocusChanges = false; RedTextCellEditor.this.removeEditorControlListeners(); } @Override public void proposalPopupClosed(final RedContentProposalAdapter adapter) { // due to GTK2 issue we're queuing new runnable to be executed after all // currently waiting operations in order to regain focus to text control // end enable focus changes handling once again SwtThread.asyncExec(new Runnable() { @Override public void run() { final Text editorControl = getEditorControl(); if (editorControl != null && !editorControl.isDisposed()) { editorControl.forceFocus(); } focusListener.handleFocusChanges = true; } }); RedTextCellEditor.this.addEditorControlListeners(); } @Override public void proposalAccepted(final RedContentProposal proposal) { // nothing to do } } }