blob: b687d0d898522c658fe3f5da06eb5354d40afd52 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 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.swing.basic.table.celleditor;
import java.awt.AWTKeyStroke;
import java.awt.Component;
import java.awt.Container;
import java.awt.DefaultFocusTraversalPolicy;
import java.awt.FocusTraversalPolicy;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.AbstractAction;
import javax.swing.AbstractCellEditor;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
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.swing.SingleLayout;
import org.eclipse.scout.rt.ui.swing.SwingUtility;
import org.eclipse.scout.rt.ui.swing.basic.ISwingInputVerifyListener;
import org.eclipse.scout.rt.ui.swing.basic.ISwingScoutComposite;
import org.eclipse.scout.rt.ui.swing.basic.table.ISwingScoutTable;
import org.eclipse.scout.rt.ui.swing.basic.table.SwingTableColumn;
import org.eclipse.scout.rt.ui.swing.ext.JPanelEx;
import org.eclipse.scout.rt.ui.swing.ext.JTextFieldEx;
import org.eclipse.scout.rt.ui.swing.focus.SwingScoutFocusTraversalPolicy;
public class SwingScoutTableCellEditor {
/**
* Property to access the table cell's insets within the inline editor. The insets are registered as client property
* in {@link JTable}.
*/
public static final String TABLE_CELL_INSETS = SwingScoutTableCellEditor.class.getName() + "#insets";
private final ISwingScoutTable m_tableComposite;
private final FocusTraversalPolicy m_focusTraversalPolicy;
private final TableCellEditor m_cellEditor;
private final CellEditorListener m_cellEditorListener;
private ISwingScoutComposite<? extends IFormField> m_cachedSwingEditor;
private P_SwingInputVerifyListener m_verifyListener;
public SwingScoutTableCellEditor(ISwingScoutTable tableComposite) {
m_tableComposite = tableComposite;
m_focusTraversalPolicy = new SwingScoutFocusTraversalPolicy();
m_cellEditor = new P_SwingCellEditor();
m_cellEditorListener = new P_CellEditorListener();
m_cellEditor.addCellEditorListener(m_cellEditorListener);
}
//(re)install cell editors
public void initialize() {
m_tableComposite.getSwingTable().setDefaultEditor(Object.class, m_cellEditor);
}
public synchronized void dispose() {
clearCachedSwingEditor();
}
protected synchronized JComponent getCachedEditorComposite(int row, int col) {
if (m_cachedSwingEditor == null) {
ISwingScoutComposite<? extends IFormField> editorComposite = createEditorComposite(row, col);
if (editorComposite != null) {
decorateEditorComposite(editorComposite, row, col);
m_verifyListener = new P_SwingInputVerifyListener();
editorComposite.addInputVerifyListener(m_verifyListener);
m_cachedSwingEditor = editorComposite;
}
else {
m_cachedSwingEditor = null;
}
}
if (m_cachedSwingEditor == null) {
return null;
}
return m_cachedSwingEditor.getSwingContainer();
}
// no need to synchronize because it is only called within already synchronized blocks
private void clearCachedSwingEditor() {
if (m_cachedSwingEditor != null) {
if (m_verifyListener != null) {
m_cachedSwingEditor.removeInputVerifyListener(m_verifyListener);
m_verifyListener = null;
}
m_cachedSwingEditor = null;
}
}
@SuppressWarnings("unchecked")
protected ISwingScoutComposite<? extends IFormField> createEditorComposite(int row, int col) {
final ITableRow scoutRow = m_tableComposite.getScoutObject().getFilteredRow(row);
final IColumn scoutColumn = m_tableComposite.getScoutObject().getColumnSet().getVisibleColumn(col);
final AtomicReference<IFormField> fieldRef = new AtomicReference<IFormField>();
if (scoutRow != null && scoutColumn != null) {
Runnable t = new Runnable() {
@Override
public void run() {
fieldRef.set(m_tableComposite.getScoutObject().getUIFacade().prepareCellEditFromUI(scoutRow, scoutColumn));
synchronized (fieldRef) {
fieldRef.notifyAll();
}
}
};
synchronized (fieldRef) {
m_tableComposite.getSwingEnvironment().invokeScoutLater(t, 2345);
try {
fieldRef.wait(2345);
}
catch (InterruptedException e) {
//nop
}
}
}
IFormField formField = fieldRef.get();
if (formField == null) {
return null;
}
// propagate insets of table cell to inline editor (to layout properly)
Insets cellInsets = new Insets(0, 0, 0, 0);
TableCellRenderer cellRenderer = m_tableComposite.getSwingTable().getCellRenderer(row, col);
cellRenderer = (TableCellRenderer) m_tableComposite.getSwingTable().prepareRenderer(cellRenderer, row, col); // do not remove this call to ensure TableCellRenderer properties (e.g. insets) really belongs to the given cell (col, row). This seems to be a bug.
if (cellRenderer instanceof DefaultTableCellRenderer) {
cellInsets = ((DefaultTableCellRenderer) cellRenderer).getInsets();
}
m_tableComposite.getSwingTable().putClientProperty(SwingScoutTableCellEditor.TABLE_CELL_INSETS, cellInsets);
try {
// propagate vertical and horizontal alignment to @{link IBooleanField} (to layout properly)
if (scoutColumn instanceof IBooleanColumn) {
GridData gd = formField.getGridDataHints();
gd.verticalAlignment = ((IBooleanColumn) scoutColumn).getVerticalAlignment();
gd.horizontalAlignment = scoutColumn.getHorizontalAlignment();
formField.setGridDataHints(gd);
}
if (formField instanceof IStringField && ((IStringField) formField).isMultilineText()) {
// for fields to be presented as popup dialog
return createEditorCompositesPopup(formField, row, col);
}
else {
return m_tableComposite.getSwingEnvironment().createFormField(m_tableComposite.getSwingTable(), formField);
}
}
finally {
m_tableComposite.getSwingTable().putClientProperty(SwingScoutTableCellEditor.TABLE_CELL_INSETS, null);
}
}
protected ISwingScoutComposite<? extends IFormField> createEditorCompositesPopup(IFormField formField, final int row, final int col) {
// 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_tableComposite.getSwingTable().getColumnModel().getColumn(col).getWidth();
int prefHeight = gd.heightInPixel;
int minHeight = Math.max(95, m_tableComposite.getSwingTable().getRowHeight(row));
prefHeight = Math.max(prefHeight, minHeight);
prefWidth = Math.max(prefWidth, minWidth);
// listener to receive events about the popup's state
final IFormFieldPopupEventListener popupListener = new IFormFieldPopupEventListener() {
@Override
public void handleEvent(FormFieldPopupEvent event) {
if ((event.getType() & FormFieldPopupEvent.TYPE_OK) > 0) {
// save cell editor
m_cellEditor.stopCellEditing();
}
else if ((event.getType() & FormFieldPopupEvent.TYPE_CANCEL) > 0) {
// cancel cell editor
m_cellEditor.cancelCellEditing();
}
// traversal control
if ((event.getType() & FormFieldPopupEvent.TYPE_FOCUS_BACK) > 0) {
enqueueEditNextTableCell(row, col, false);
}
else if ((event.getType() & FormFieldPopupEvent.TYPE_FOCUS_NEXT) > 0) {
enqueueEditNextTableCell(row, col, true);
}
}
};
// create placeholder field to represent the cell editor
JPanel cellEditorPanel = new JPanel();
cellEditorPanel.setOpaque(false);
// create popup dialog to wrap the form field
final SwingScoutFormFieldPopup formFieldDialog = new SwingScoutFormFieldPopup(cellEditorPanel);
formFieldDialog.setMinHeight(minHeight);
formFieldDialog.setMinWidth(minWidth);
formFieldDialog.setPrefHeight(prefHeight);
formFieldDialog.setPrefWidth(prefWidth);
formFieldDialog.createField(formField, m_tableComposite.getSwingEnvironment());
formFieldDialog.addEventListener(popupListener);
/*
* Wrap 'default cell editor listener' to intercept events on the cell editor.
* This is crucial because if the user clicks on another editable cell, its cell-editor is activated prior
* to the popup receives the WINDOW-CLOSED event (which simply is a mouse pressed event outside the dialog's boundaries) to
* properly close the popup and write its value back to the model. In consequence, the model is not updated with the new value.
*/
m_cellEditor.removeCellEditorListener(m_cellEditorListener);
m_cellEditor.addCellEditorListener(new P_CellEditorListener() {
@Override
public void editingStopped(ChangeEvent e) {
closePopup(FormFieldPopupEvent.TYPE_OK);
restoreCellEditorListener();
// delegate event to default cell editor listener
super.editingStopped(e);
}
@Override
public void editingCanceled(ChangeEvent e) {
closePopup(FormFieldPopupEvent.TYPE_CANCEL);
restoreCellEditorListener();
// delegate event to default cell editor listener
super.editingCanceled(e);
}
private void closePopup(int popupEvent) {
if (formFieldDialog.isClosed()) {
return;
}
// remove popup listener to not receive events on the dialog's state because the cell editor is already closing
formFieldDialog.removeEventListener(popupListener);
// close the popup
formFieldDialog.closePopup(popupEvent);
}
private void restoreCellEditorListener() {
// uninstall wrapper cell editor listener to not intercept cell editor events anymore
m_cellEditor.removeCellEditorListener(this);
// install default cell editor listener
m_cellEditor.addCellEditorListener(m_cellEditorListener);
}
});
return formFieldDialog;
}
protected void decorateEditorComposite(ISwingScoutComposite<? extends IFormField> editorComposite, final int row, final int col) {
JComponent editorField = editorComposite.getSwingContainer();
Component firstField = m_focusTraversalPolicy.getFirstComponent(editorField);
Component lastField = m_focusTraversalPolicy.getLastComponent(editorField);
if (firstField != null) {
firstField.addHierarchyListener(new HierarchyListener() {
@Override
public void hierarchyChanged(final HierarchyEvent e) {
if (e.getID() == HierarchyEvent.HIERARCHY_CHANGED) {
if (((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) && e.getComponent().isShowing()) {
SwingUtilities.invokeLater(
new Runnable() {
@Override
public void run() {
e.getComponent().requestFocus();
}
});
}
}
}
});
}
if (firstField instanceof JComponent) {
JComponent jc = (JComponent) firstField;
jc.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, new HashSet<AWTKeyStroke>());
jc.getInputMap(JComponent.WHEN_FOCUSED).put(SwingUtility.createKeystroke("shift TAB"), "reverse-tab");
jc.getActionMap().put("reverse-tab", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
m_cellEditor.stopCellEditing();
enqueueEditNextTableCell(row, col, false);
}
});
}
if (lastField instanceof JComponent) {
JComponent jc = (JComponent) lastField;
jc.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, new HashSet<AWTKeyStroke>());
jc.getInputMap(JComponent.WHEN_FOCUSED).put(SwingUtility.createKeystroke("TAB"), "tab");
jc.getActionMap().put("tab", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
m_cellEditor.stopCellEditing();
enqueueEditNextTableCell(row, col, true);
}
});
}
}
protected synchronized void saveEditorFromSwing() {
if (m_cachedSwingEditor != null) {
clearCachedSwingEditor();
Runnable t = new Runnable() {
@Override
public void run() {
m_tableComposite.getScoutObject().getUIFacade().completeCellEditFromUI();
}
};
m_tableComposite.getSwingEnvironment().invokeScoutLater(t, 0);
}
}
protected boolean isBooleanColumnAt(Point p) {
JTable table = m_tableComposite.getSwingTable();
int col = table.columnAtPoint(p);
if (col >= 0) {
TableColumn tc = table.getColumnModel().getColumn(col);
if (tc instanceof SwingTableColumn) {
IColumn<?> scoutCol = ((SwingTableColumn) tc).getScoutColumn();
return (scoutCol instanceof IBooleanColumn);
}
}
return false;
}
protected synchronized void cancelEditorFromSwing() {
if (m_cachedSwingEditor != null) {
clearCachedSwingEditor();
Runnable t = new Runnable() {
@Override
public void run() {
m_tableComposite.getScoutObject().getUIFacade().cancelCellEditFromUI();
}
};
m_tableComposite.getSwingEnvironment().invokeScoutLater(t, 0);
}
}
protected void enqueueEditNextTableCell(int uiRow, int uiCol, final boolean forward) {
if (uiRow < 0 || uiCol < 0) {
return;
}
final ITableRow row = m_tableComposite.getScoutObject().getFilteredRow(uiRow);
final IColumn col = m_tableComposite.getScoutObject().getColumnSet().getVisibleColumn(uiCol);
if (row == null || col == null) {
return;
}
m_tableComposite.getSwingEnvironment().invokeScoutLater(new Runnable() {
@Override
public void run() {
if (m_tableComposite.getSwingEnvironment() == null) {
return;
}
ITable table = m_tableComposite.getScoutObject();
TableUtility.editNextTableCell(table, row, col, forward, null);
}
}, 0L);
}
private final class P_SwingInputVerifyListener implements ISwingInputVerifyListener {
@Override
public void verify(JComponent input) {
if (input instanceof JTextFieldEx) {
if (((JTextFieldEx) input).isShowingPopup()) {
return;
}
}
saveEditorFromSwing();
}
}
private class P_SwingCellEditor extends AbstractCellEditor implements TableCellEditor {
private static final long serialVersionUID = 1L;
/**
* An integer specifying the number of clicks needed to start editing.
* Even if <code>clickCountToStart</code> is defined as zero, it
* will not initiate until a click occurs.
*/
private int m_clickCountToStart = 1;
private final JPanelEx m_container;
public P_SwingCellEditor() {
m_container = new JPanelEx(new SingleLayout());
SwingUtility.installFocusCycleRoot(m_container, createEditorTraversalPolicy());
m_container.setOpaque(false);
m_container.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(SwingUtility.createKeystroke("ESCAPE"), "cancel");
m_container.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(SwingUtility.createKeystroke("ENTER"), "enter");
m_container.getActionMap().put("cancel", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
m_cellEditor.cancelCellEditing();
}
});
m_container.getActionMap().put("enter", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
m_cellEditor.stopCellEditing();
}
});
}
/**
* Creates a different focus traversal policy that makes sure that the focus is not set outside the table, after an
* editor is removed. Verify input after the focus is lost. {@link SwingScoutFocusTraversalPolicy}
*/
private DefaultFocusTraversalPolicy createEditorTraversalPolicy() {
DefaultFocusTraversalPolicy policy = new DefaultFocusTraversalPolicy() {
private static final long serialVersionUID = -8422385781848194341L;
@Override
public Component getComponentAfter(Container focusCycleRoot, Component aComponent) {
if (aComponent != null) {
boolean accept = SwingUtility.runInputVerifier(aComponent);
if (!accept) {
return aComponent;
}
}
return super.getComponentAfter(focusCycleRoot, aComponent);
}
};
return policy;
}
public void setClickCountToStart(int count) {
m_clickCountToStart = count;
}
public int getClickCountToStart() {
return m_clickCountToStart;
}
@Override
public Object getCellEditorValue() {
return null;
}
@Override
public boolean isCellEditable(EventObject e) {
if (e instanceof MouseEvent) {
//no edit on boolean column when mouse was clicked
if (isBooleanColumnAt(((MouseEvent) e).getPoint())) {
return false;
}
return ((MouseEvent) e).getClickCount() >= getClickCountToStart();
}
return true;
}
@Override
public Component getTableCellEditorComponent(final JTable table, Object value, boolean isSelected, final int row, final int column) {
m_container.removeAll();
Component c = getCachedEditorComposite(row, column);
if (c != null) {
m_container.add(c);
}
return m_container;
}
}
private class P_CellEditorListener implements CellEditorListener {
@Override
public void editingStopped(ChangeEvent e) {
saveEditorFromSwing();
}
@Override
public void editingCanceled(ChangeEvent e) {
cancelEditorFromSwing();
}
}
}