blob: 2430bcf0373d9859a5471316ded18c8bd83d3fdc [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0.
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
# IBM Corporation - org.eclipse.jface.viewer: initial API and implementation of TextCellEditor
# Tom Eicher <eclipse@tom.eicher.name> - org.eclipse.jface.viewer: fix minimum width
#=============================================================================*/
package org.eclipse.statet.ecommons.ui.components;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;
/**
* A cell editor that manages a text entry field with tools button.
* The cell editor's value is the text string itself.
* <p>
*/
public abstract class ExtensibleTextCellEditor extends CellEditor {
public static abstract class FocusGroup implements Listener {
private final List<Widget> fControls= new ArrayList<>();
private Widget fWidget;
private int fIgnore;
@Override
public void handleEvent(final Event event) {
Widget control;
switch (event.type) {
case SWT.Activate:
control= Display.getCurrent().getFocusControl();
this.fWidget= (event.widget != control && this.fControls.contains(control)) ? control : null;
return;
case SWT.FocusIn:
this.fWidget= null;
return;
case SWT.FocusOut:
control= this.fWidget;
this.fWidget= null;
if (this.fIgnore == 0 && event.widget != control) {
focusLost();
}
return;
}
}
protected abstract void focusLost();
public void add(final Control control) {
this.fControls.add(control);
control.addListener(SWT.Activate, this);
control.addListener(SWT.FocusIn, this);
control.addListener(SWT.FocusOut, this);
}
public void addRecursivly(final Control control) {
if (control instanceof Composite) {
final Control[] children= ((Composite) control).getChildren();
for (final Control child : children) {
addRecursivly(child);
}
}
else {
add(control);
}
}
public void discontinueTracking() {
this.fIgnore++;
}
public void continueTracking() {
this.fIgnore--;
}
}
/**
* Default TextCellEditor style
* specify no borders on text widget as cell outline in table already
* provides the look of a border.
*/
protected static final int DEFAULT_STYLE= SWT.SINGLE;
/**
* The text control; initially <code>null</code>.
*/
protected Text text;
private ModifyListener modifyListener;
/**
* State information for updating action enablement
*/
private boolean isSelection= false;
private boolean isDeleteable= false;
private boolean isSelectable= false;
private FocusGroup focusGroup;
/**
* Creates a new text string cell editor parented under the given control.
* The cell editor value is the string itself, which is initially the empty string.
* Initially, the cell editor has no cell validator.
*
* @param parent the parent control
*/
public ExtensibleTextCellEditor(final Composite parent) {
super(parent);
}
/**
* Checks to see if the "deletable" state (can delete/
* nothing to delete) has changed and if so fire an
* enablement changed notification.
*/
private void checkDeleteable() {
final boolean oldIsDeleteable= this.isDeleteable;
this.isDeleteable= isDeleteEnabled();
if (oldIsDeleteable != this.isDeleteable) {
fireEnablementChanged(DELETE);
}
}
/**
* Checks to see if the "selectable" state (can select)
* has changed and if so fire an enablement changed notification.
*/
private void checkSelectable() {
final boolean oldIsSelectable= this.isSelectable;
this.isSelectable= isSelectAllEnabled();
if (oldIsSelectable != this.isSelectable) {
fireEnablementChanged(SELECT_ALL);
}
}
/**
* Checks to see if the selection state (selection /
* no selection) has changed and if so fire an
* enablement changed notification.
*/
private void checkSelection() {
final boolean oldIsSelection= this.isSelection;
this.isSelection= this.text.getSelectionCount() > 0;
if (oldIsSelection != this.isSelection) {
fireEnablementChanged(COPY);
fireEnablementChanged(CUT);
}
}
@Override
protected Control createControl(final Composite parent) {
final Control control= createCustomControl(parent);
this.text.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(final SelectionEvent e) {
handleDefaultSelection(e);
}
});
this.text.addKeyListener(new KeyAdapter() {
// hook key pressed - see PR 14201
@Override
public void keyPressed(final KeyEvent e) {
keyReleaseOccured(e);
// as a result of processing the above call, clients may have
// disposed this cell editor
if ((getControl() == null) || getControl().isDisposed()) {
return;
}
checkSelection(); // see explanation below
checkDeleteable();
checkSelectable();
}
});
this.text.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(final TraverseEvent e) {
if (e.detail == SWT.TRAVERSE_ESCAPE
|| e.detail == SWT.TRAVERSE_RETURN) {
e.doit= false;
}
}
});
// We really want a selection listener but it is not supported so we
// use a key listener and a mouse listener to know when selection changes
// may have occurred
this.text.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(final MouseEvent e) {
checkSelection();
checkDeleteable();
checkSelectable();
}
});
this.text.setFont(parent.getFont());
this.text.setBackground(parent.getBackground());
this.text.setText("");//$NON-NLS-1$
this.text.addModifyListener(getModifyListener());
this.text.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
this.focusGroup= new FocusGroup() {
@Override
protected void focusLost() {
ExtensibleTextCellEditor.this.focusLost();
}
};
this.focusGroup.addRecursivly(control);
return control;
}
protected FocusGroup getFocusGroup() {
return this.focusGroup;
}
protected abstract Control createCustomControl(final Composite parent);
/**
* The <code>TextCellEditor</code> implementation of
* this <code>CellEditor</code> framework method returns
* the text string.
*
* @return the text string
*/
@Override
protected Object doGetValue() {
return this.text.getText();
}
@Override
protected void doSetFocus() {
if (this.text != null) {
this.text.selectAll();
this.text.setFocus();
checkSelection();
checkDeleteable();
checkSelectable();
}
}
/**
* The <code>TextCellEditor</code> implementation of
* this <code>CellEditor</code> framework method accepts
* a text string (type <code>String</code>).
*
* @param value a text string (type <code>String</code>)
*/
@Override
protected void doSetValue(final Object value) {
Assert.isTrue(this.text != null && (value instanceof String));
this.text.removeModifyListener(getModifyListener());
this.text.setText((String) value);
this.text.addModifyListener(getModifyListener());
}
/**
* Processes a modify event that occurred in this text cell editor.
* This framework method performs validation and sets the error message
* accordingly, and then reports a change via <code>fireEditorValueChanged</code>.
* Subclasses should call this method at appropriate times. Subclasses
* may extend or reimplement.
*
* @param e the SWT modify event
*/
protected void editOccured(final ModifyEvent e) {
String value= this.text.getText();
if (value == null) {
value= "";//$NON-NLS-1$
}
final Object typedValue= value;
final boolean oldValidState= isValueValid();
final boolean newValidState= isCorrect(typedValue);
if (!newValidState) {
// try to insert the current value into the error message.
setErrorMessage(MessageFormat.format(getErrorMessage(),
new Object[] { value }));
}
valueChanged(oldValidState, newValidState);
}
/**
* Since a text editor field is scrollable we don't
* set a minimumSize.
*/
@Override
public LayoutData getLayoutData() {
final LayoutData layoutData= new LayoutData();
layoutData.minimumWidth= 10;
return layoutData;
}
/**
* Return the modify listener.
*/
private ModifyListener getModifyListener() {
if (this.modifyListener == null) {
this.modifyListener= new ModifyListener() {
@Override
public void modifyText(final ModifyEvent e) {
editOccured(e);
}
};
}
return this.modifyListener;
}
/**
* Handles a default selection event from the text control by applying the editor
* value and deactivating this cell editor.
*
* @param event the selection event
*
* @since 3.0
*/
protected void handleDefaultSelection(final SelectionEvent event) {
// same with enter-key handling code in keyReleaseOccured(e);
fireApplyEditorValue();
deactivate();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method returns <code>true</code> if
* the current selection is not empty.
*/
@Override
public boolean isCopyEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return this.text.getSelectionCount() > 0;
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method returns <code>true</code> if
* the current selection is not empty.
*/
@Override
public boolean isCutEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return this.text.getSelectionCount() > 0;
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method returns <code>true</code>
* if there is a selection or if the caret is not positioned
* at the end of the text.
*/
@Override
public boolean isDeleteEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return this.text.getSelectionCount() > 0
|| this.text.getCaretPosition() < this.text.getCharCount();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method always returns <code>true</code>.
*/
@Override
public boolean isPasteEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return true;
}
/**
* Check if save all is enabled
* @return true if it is
*/
public boolean isSaveAllEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return true;
}
/**
* Returns <code>true</code> if this cell editor is
* able to perform the select all action.
* <p>
* This default implementation always returns
* <code>false</code>.
* </p>
* <p>
* Subclasses may override
* </p>
* @return <code>true</code> if select all is possible,
* <code>false</code> otherwise
*/
@Override
public boolean isSelectAllEnabled() {
if (this.text == null || this.text.isDisposed()) {
return false;
}
return this.text.getCharCount() > 0;
}
/**
* Processes a key release event that occurred in this cell editor.
* <p>
* The <code>TextCellEditor</code> implementation of this framework method
* ignores when the RETURN key is pressed since this is handled in
* <code>handleDefaultSelection</code>.
* An exception is made for Ctrl+Enter for multi-line texts, since
* a default selection event is not sent in this case.
* </p>
*
* @param keyEvent the key event
*/
@Override
protected void keyReleaseOccured(final KeyEvent keyEvent) {
if (keyEvent.character == '\r') { // Return key
// Enter is handled in handleDefaultSelection.
// Do not apply the editor value in response to an Enter key event
// since this can be received from the IME when the intent is -not-
// to apply the value.
// See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control
//
// An exception is made for Ctrl+Enter for multi-line texts, since
// a default selection event is not sent in this case.
if (this.text != null && !this.text.isDisposed()
&& (this.text.getStyle() & SWT.MULTI) != 0) {
if ((keyEvent.stateMask & SWT.CTRL) != 0) {
super.keyReleaseOccured(keyEvent);
}
}
return;
}
super.keyReleaseOccured(keyEvent);
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method copies the
* current selection to the clipboard.
*/
@Override
public void performCopy() {
this.text.copy();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method cuts the
* current selection to the clipboard.
*/
@Override
public void performCut() {
this.text.cut();
checkSelection();
checkDeleteable();
checkSelectable();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method deletes the
* current selection or, if there is no selection,
* the character next character from the current position.
*/
@Override
public void performDelete() {
if (this.text.getSelectionCount() > 0) {
// remove the contents of the current selection
this.text.insert(""); //$NON-NLS-1$
} else {
// remove the next character
final int pos= this.text.getCaretPosition();
if (pos < this.text.getCharCount()) {
this.text.setSelection(pos, pos + 1);
this.text.insert(""); //$NON-NLS-1$
}
}
checkSelection();
checkDeleteable();
checkSelectable();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method pastes the
* the clipboard contents over the current selection.
*/
@Override
public void performPaste() {
this.text.paste();
checkSelection();
checkDeleteable();
checkSelectable();
}
/**
* The <code>TextCellEditor</code> implementation of this
* <code>CellEditor</code> method selects all of the
* current text.
*/
@Override
public void performSelectAll() {
this.text.selectAll();
checkSelection();
checkDeleteable();
}
@Override
protected boolean dependsOnExternalFocusListener() {
return false;
}
protected void fillToolsMenu(final Menu menu) {
}
}