blob: dcab3676a7a67354ebe73375cb54c550a177caa2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 BSI Business Systems Integration AG.
* 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:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.ui.swt.basic.table.celleditor;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorDeactivationEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.scout.commons.holders.Holder;
import org.eclipse.scout.commons.logger.IScoutLogger;
import org.eclipse.scout.commons.logger.ScoutLogManager;
import org.eclipse.scout.rt.client.ui.basic.table.ITable;
import org.eclipse.scout.rt.client.ui.basic.table.ITableRow;
import org.eclipse.scout.rt.client.ui.basic.table.TableUtility;
import org.eclipse.scout.rt.client.ui.basic.table.columns.IBooleanColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn;
import org.eclipse.scout.rt.client.ui.form.fields.GridData;
import org.eclipse.scout.rt.client.ui.form.fields.IFormField;
import org.eclipse.scout.rt.client.ui.form.fields.stringfield.IStringField;
import org.eclipse.scout.rt.ui.swt.ISwtEnvironment;
import org.eclipse.scout.rt.ui.swt.basic.ISwtScoutComposite;
import org.eclipse.scout.rt.ui.swt.basic.table.ISwtScoutTable;
import org.eclipse.scout.rt.ui.swt.basic.table.SwtScoutTable;
import org.eclipse.scout.rt.ui.swt.keystroke.SwtKeyStroke;
import org.eclipse.scout.rt.ui.swt.util.SwtUtility;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Widget;
/**
* <p>
* {@link CellEditor} for {@link SwtScoutTable}.
* </p>
* Each editable cell has its own {@link CellEditor} instance.
*/
public class TableCellEditor extends CellEditor {
private static final IScoutLogger LOG = ScoutLogManager.getLogger(TableCellEditor.class);
private ISwtEnvironment m_environment;
private P_FocusLostListener m_focusLostListener;
private Composite m_container;
private Image m_image;
private IColumn<?> m_scoutColumn;
private ITableRow m_scoutRow;
private ITable m_scoutTable;
private TableColumn m_swtColumn;
private Table m_swtTable;
private TableViewer m_tableViewer;
private boolean m_requestFocus;
public TableCellEditor(TableViewer tableViewer, TableColumn swtColumn, ITableRow scoutRow, ISwtEnvironment environment) {
super(tableViewer.getTable());
m_scoutRow = scoutRow;
m_scoutColumn = (IColumn<?>) swtColumn.getData(ISwtScoutTable.KEY_SCOUT_COLUMN);
m_scoutTable = m_scoutColumn.getTable();
m_swtColumn = swtColumn;
m_tableViewer = tableViewer;
m_swtTable = tableViewer.getTable();
m_environment = environment;
m_focusLostListener = new P_FocusLostListener();
}
@Override
protected Control createControl(Composite parent) {
m_container = new Composite(parent, SWT.NONE) {
@Override
// disable inner components preferred sizes.
public Point computeSize(int wHint, int hHint, boolean changed) {
return new Point(wHint, hHint);
}
};
m_container.setLayout(new FillLayout());
return m_container;
}
@Override
protected Object doGetValue() {
// NOOP: The value is written back to the model by the widget's verify event.
return null;
}
@Override
protected void doSetValue(Object value) {
// NOOP: The value is set into the cell-editor when it is created.
}
@Override
public void activate(ColumnViewerEditorActivationEvent e) {
m_requestFocus = true;
// Install a focus-lost listener on the table widget to close an active cell-editor when the table looses the focus.
m_focusLostListener.install();
// Install keystrokes to exit editing mode.
m_environment.addKeyStroke(m_container, new SwtKeyStroke(SWT.ESC) {
@Override
public void handleSwtAction(Event event) {
event.doit = false;
fireCancelEditor();
}
});
m_environment.addKeyStroke(m_container, new SwtKeyStroke(SWT.CR) {
@Override
public void handleSwtAction(Event event) {
event.doit = false;
fireApplyEditorValue();
}
});
m_environment.addKeyStroke(m_container, new SwtKeyStroke(SWT.KEYPAD_CR) {
@Override
public void handleSwtAction(Event event) {
event.doit = false;
fireApplyEditorValue();
}
});
// Specific cell-editor for boolean values.
if (m_scoutColumn instanceof IBooleanColumn) {
if (e.sourceEvent instanceof MouseEvent) {
// no edit-mode when a boolean cell was clicked by mouse because being inverted and the editing mode closed in AbstractTable#interceptRowClickSingleObserver.
m_requestFocus = false;
return;
}
else {
// hide the checkbox image when editing a boolean value in traversal-mode.
ViewerCell cell = (ViewerCell) e.getSource();
m_image = cell.getImage();
cell.setImage(null);
}
}
// create the Scout model field.
IFormField formField = createFormField();
if (formField == null) {
LOG.warn("Failed to create FormField for cell-editor; editing mode canceled.");
m_requestFocus = false;
fireCancelEditor();
return;
}
// create the UI field.
ISwtScoutComposite swtScoutFormField;
if (formField instanceof IStringField && ((IStringField) formField).isMultilineText()) {
// open a separate Shell to edit the content.
swtScoutFormField = createPopupEditorControl(m_container, formField);
}
else {
swtScoutFormField = m_environment.createFormField(m_container, formField);
}
// hook to customize the form field.
decorateEditorComposite(swtScoutFormField, m_scoutRow, m_scoutColumn);
m_container.layout(true, true);
m_container.setVisible(true);
}
@Override
protected void doSetFocus() {
if (!m_requestFocus) {
return;
}
// traverse the focus to the cell editor's control so that the user can start editing immediately without having to click into the widget first.
m_container.traverse(SWT.TRAVERSE_TAB_NEXT);
Control focusControl = m_container.getDisplay().getFocusControl();
if (focusControl != null && SwtUtility.isAncestorOf(m_container, focusControl)) {
focusControl.addTraverseListener(new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent e) {
switch (e.detail) {
case SWT.TRAVERSE_ESCAPE:
case SWT.TRAVERSE_RETURN: {
e.doit = false;
break;
}
case SWT.TRAVERSE_TAB_NEXT: {
e.doit = false;
fireApplyEditorValue();
enqueueEditNextTableCell(true); // traverse the focus to the next editable cell.
break;
}
case SWT.TRAVERSE_TAB_PREVIOUS: {
e.doit = false;
fireApplyEditorValue();
enqueueEditNextTableCell(false); // traverse the focus to the next editable cell.
break;
}
}
}
});
}
}
@Override
protected void deactivate(ColumnViewerEditorDeactivationEvent e) {
// restore the cell's image if being unset in CellEditor#activate.
ViewerCell cell = (ViewerCell) e.getSource();
if (m_image != null) {
cell.setImage(m_image);
}
m_image = null;
// Dispose the cell-editor; in turn, any Shell opened by the editor is closed as well.
for (Control c : m_container.getChildren()) {
c.dispose();
}
super.deactivate(e);
m_focusLostListener.uninstall();
}
@Override
protected boolean dependsOnExternalFocusListener() {
return false;
}
protected void enqueueEditNextTableCell(final boolean forward) {
m_environment.invokeScoutLater(new Runnable() {
@Override
public void run() {
ITable table = m_scoutColumn.getTable();
TableUtility.editNextTableCell(table, m_scoutRow, m_scoutColumn, forward, new TableUtility.ITableCellEditorFilter() {
@Override
public boolean accept(ITableRow rowx, IColumn<?> colx) {
return true;
}
});
}
}, 0L);
}
protected ISwtScoutComposite<? extends IFormField> createPopupEditorControl(final Composite parent, IFormField formField) {
// overwrite layout properties
GridData gd = formField.getGridData();
gd.h = 1;
gd.w = IFormField.FULL_WIDTH;
gd.weightY = 1;
gd.weightX = 1;
formField.setGridDataInternal(gd);
int prefWidth = gd.widthInPixel;
int minWidth = m_swtColumn.getWidth();
int prefHeight = gd.heightInPixel;
int minHeight = Math.max(105, m_swtTable.getItemHeight());
prefHeight = Math.max(prefHeight, minHeight);
prefWidth = Math.max(prefWidth, minWidth);
// Create placeholder field to represent the cell editor
final Composite cellEditorComposite = new Composite(parent, SWT.NONE);
// Create popup dialog to wrap the form field
final SwtScoutFormFieldPopup popup = new SwtScoutFormFieldPopup(cellEditorComposite);
popup.setPrefHeight(prefHeight);
popup.setPrefWidth(prefWidth);
popup.setMinHeight(minHeight);
popup.setMinWidth(minWidth);
// Focus is set the time the Shell is opened.
m_requestFocus = false;
// == IFormFieldPopupListener ==
// To receive events about the popup's state. The popup is not closed yet but the cell-editor closed.
final IFormFieldPopupListener formFieldPopupListener = new IFormFieldPopupListener() {
@Override
public void handleEvent(int event) {
if ((event & IFormFieldPopupListener.TYPE_OK) > 0) {
SwtUtility.runSwtInputVerifier(popup.getSwtField()); // write the value back into the model.
fireApplyEditorValue();
}
else if ((event & IFormFieldPopupListener.TYPE_CANCEL) > 0) {
fireCancelEditor();
}
// traversal control
if ((event & IFormFieldPopupListener.TYPE_FOCUS_BACK) > 0) {
enqueueEditNextTableCell(false);
}
else if ((event & IFormFieldPopupListener.TYPE_FOCUS_NEXT) > 0) {
enqueueEditNextTableCell(true);
}
}
};
popup.addListener(formFieldPopupListener);
// == DisposeListener ==
// To close the Shell if the cell-editor is disposed.
cellEditorComposite.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
popup.removeListener(formFieldPopupListener); // ignore resulting popup events.
// Close the popup Shell.
// The asyncExec is a workaround so that other cell-editors can be activated immediately.
// Note: If being dirty, 'Viewer#refresh()' in TableEx prevents the cell from being activated immediately.
e.display.asyncExec(new Runnable() {
@Override
public void run() {
popup.closePopup();
}
});
}
});
// Open the popup for the form field.
popup.createField(parent, formField, m_environment);
return popup;
}
protected IFormField createFormField() {
final Holder<IFormField> result = new Holder<IFormField>();
Runnable t = new Runnable() {
@Override
public void run() {
result.setValue(m_scoutTable.getUIFacade().prepareCellEditFromUI(m_scoutRow, m_scoutColumn));
}
};
try {
m_environment.invokeScoutLater(t, 2345).join(2345);
}
catch (InterruptedException e) {
LOG.warn("Interrupted while waiting for the Form-Field to be created.", e);
}
return result.getValue();
}
/**
* Callback to be overwritten to customize the {@link IFormField}.
*/
protected void decorateEditorComposite(ISwtScoutComposite editorComposite, final ITableRow scoutRow, final IColumn<?> scoutCol) {
}
/**
* Hysteresis listener that commits the cell editor when the table has first received focus and then lost it. That is
* because cell editors in SWT are not closed automatically if the table looses the focus.
*/
private class P_FocusLostListener implements Listener {
/**
* Installs listening for focus-lost events on the table widget.
*/
public void install() {
m_environment.getDisplay().addFilter(SWT.FocusIn, this);
}
/**
* Uninstalls listening for focus-lost events on the table widget.
*/
public void uninstall() {
m_environment.getDisplay().removeFilter(SWT.FocusIn, this);
}
@Override
public void handleEvent(Event event) {
Widget w = event.widget;
if (w == null || !(w instanceof Control) || w.isDisposed()) {
return;
}
// Sanity check whether a cell-editor is active.
TableViewer viewer = m_tableViewer;
if (!viewer.isCellEditorActive()) {
return;
}
Control focusOwner = (Control) w;
Table table = m_tableViewer.getTable();
// Check if the table is the focus owner.
if (SwtUtility.isAncestorOf(table, focusOwner)) {
return;
}
// Check if a Shell opened by the cell-editor is the focus owner.
if (focusOwner.getShell() != table.getShell()) {
Composite parentFocusOwner = focusOwner.getShell().getParent();
while (parentFocusOwner != null) {
if (parentFocusOwner.getShell() == table.getShell()) {
return; // focus owner is a derrived Shell.
}
else {
parentFocusOwner = parentFocusOwner.getShell().getParent();
}
}
}
// Close the cell-editor because a control other than the table is focus owner.
fireApplyEditorValue();
}
}
}