blob: 097b4bf48978773ea77f7055cae807cb26089b3b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 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.form.fields.smartfield;
import java.beans.PropertyChangeEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.scout.commons.CompareUtility;
import org.eclipse.scout.commons.exception.ProcessingException;
import org.eclipse.scout.commons.holders.Holder;
import org.eclipse.scout.commons.job.JobEx;
import org.eclipse.scout.commons.logger.IScoutLogger;
import org.eclipse.scout.commons.logger.ScoutLogManager;
import org.eclipse.scout.rt.client.ui.action.IAction;
import org.eclipse.scout.rt.client.ui.action.menu.IMenu;
import org.eclipse.scout.rt.client.ui.form.FormEvent;
import org.eclipse.scout.rt.client.ui.form.FormListener;
import org.eclipse.scout.rt.client.ui.form.fields.smartfield.ISmartField;
import org.eclipse.scout.rt.client.ui.form.fields.smartfield.ISmartFieldProposalForm;
import org.eclipse.scout.rt.ui.swt.LogicalGridLayout;
import org.eclipse.scout.rt.ui.swt.SwtMenuUtility;
import org.eclipse.scout.rt.ui.swt.action.AbstractSwtScoutActionPropertyChangeListener;
import org.eclipse.scout.rt.ui.swt.ext.DropDownButton;
import org.eclipse.scout.rt.ui.swt.ext.StatusLabelEx;
import org.eclipse.scout.rt.ui.swt.form.fields.IPopupSupport;
import org.eclipse.scout.rt.ui.swt.form.fields.LogicalGridDataBuilder;
import org.eclipse.scout.rt.ui.swt.form.fields.SwtScoutValueFieldComposite;
import org.eclipse.scout.rt.ui.swt.internal.TextFieldEditableSupport;
import org.eclipse.scout.rt.ui.swt.keystroke.SwtKeyStroke;
import org.eclipse.scout.rt.ui.swt.util.SwtLayoutUtility;
import org.eclipse.scout.rt.ui.swt.util.SwtUtility;
import org.eclipse.scout.rt.ui.swt.window.SwtScoutPartEvent;
import org.eclipse.scout.rt.ui.swt.window.SwtScoutPartListener;
import org.eclipse.scout.rt.ui.swt.window.popup.SwtScoutDropDownPopup;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
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.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
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.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.Widget;
/**
* <h3>SwtScoutSmartField</h3> ...
*
* @since 1.0.0 10.04.2008
*/
public class SwtScoutSmartField extends SwtScoutValueFieldComposite<ISmartField<?>> implements ISwtScoutSmartField, IPopupSupport {
private static final IScoutLogger LOG = ScoutLogManager.getLogger(SwtScoutSmartField.class);
private DropDownButton m_browseButton;
private P_PendingProposalJob m_pendingProposalJob;
private Object m_pendingProposalJobLock;
// popup
private SwtScoutDropDownPopup m_proposalPopup;
private final Object m_popupLock = new Object();
private Menu m_contextMenu;
private TextFieldEditableSupport m_editableSupport;
private Set<IPopupSupportListener> m_popupEventListeners;
private Object m_popupEventListenerLock;
private P_ScoutActionPropertyChangeListener m_scoutActionPropertyListener;
public SwtScoutSmartField() {
m_pendingProposalJobLock = new Object();
}
@Override
protected void initializeSwt(Composite parent) {
m_popupEventListeners = new HashSet<IPopupSupportListener>();
m_popupEventListenerLock = new Object();
Composite container = getEnvironment().getFormToolkit().createComposite(parent);
StatusLabelEx label = getEnvironment().getFormToolkit().createStatusLabel(container, getEnvironment(), getScoutObject());
StyledText textField = getEnvironment().getFormToolkit().createStyledText(container, SWT.SINGLE | SWT.BORDER);
m_browseButton = new DropDownButton(container, SWT.DROP_DOWN);
// to ensure the text is validated on a context menu call this mouse
// listener is used.
m_browseButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
handleSwtInputVerifier();
}
});
setSwtContainer(container);
setSwtLabel(label);
setSwtField(textField);
// prevent the button from grabbing focus
container.setTabList(new Control[]{textField});
m_browseButton.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
getSwtField().setFocus();
}
});
// context menu
m_contextMenu = new Menu(m_browseButton.getShell(), SWT.POP_UP);
m_contextMenu.addMenuListener(new P_ContextMenuListener());
m_browseButton.setMenu(m_contextMenu);
// F2 key stroke
getEnvironment().addKeyStroke(getSwtContainer(), new P_F2KeyStroke());
// listeners
P_UiFieldListener listener = new P_UiFieldListener();
getSwtField().addListener(SWT.KeyDown, listener);
getSwtField().addListener(SWT.Modify, listener);
getSwtField().addListener(SWT.Traverse, listener);
P_SwtBrowseButtonListener swtBrowseButtonListener = new P_SwtBrowseButtonListener();
getSwtBrowseButton().addSelectionListener(swtBrowseButtonListener);
// layout
container.setLayout(new LogicalGridLayout(1, 0));
m_browseButton.setLayoutData(LogicalGridDataBuilder.createSmartButton());
}
@Override
public DropDownButton getSwtBrowseButton() {
return m_browseButton;
}
@Override
public StyledText getSwtField() {
return (StyledText) super.getSwtField();
}
@Override
protected void attachScout() {
super.attachScout();
setIconIdFromScout(getScoutObject().getIconId());
setProposalFormFromScout(getScoutObject().getProposalForm());
if (m_scoutActionPropertyListener == null) {
m_scoutActionPropertyListener = new P_ScoutActionPropertyChangeListener();
}
m_scoutActionPropertyListener.attachToScoutMenus(getScoutObject().getMenus());
}
@Override
protected void detachScout() {
// workaround since disposeFieldInternal in AbstractSmartField is never called.
hideProposalPopup();
if (m_scoutActionPropertyListener != null) {
m_scoutActionPropertyListener.detachFromScoutMenus(getScoutObject().getMenus());
m_scoutActionPropertyListener = null;
}
super.detachScout();
}
@Override
protected void setDisplayTextFromScout(String s) {
getSwtBrowseButton().setDropdownEnabled(calculateDropDownButtonEnabled());
if (!CompareUtility.equals(s, getSwtField().getText())) {
if (s == null) {
s = "";
}
StyledText swtField = getSwtField();
swtField.setText(s);
swtField.setCaretOffset(0);
}
}
private boolean calculateDropDownButtonEnabled() {
final AtomicBoolean hasValidMenus = new AtomicBoolean(false);
Runnable t = new Runnable() {
@Override
public void run() {
hasValidMenus.set(getScoutObject().getUIFacade().hasValidMenusFromUI());
}
};
JobEx job = getEnvironment().invokeScoutLater(t, 1200);
try {
job.join(1200);
}
catch (InterruptedException ex) {
//nop
}
return hasValidMenus.get();
}
@Override
protected void setEnabledFromScout(boolean b) {
super.setEnabledFromScout(b);
m_browseButton.setButtonEnabled(b);
getSwtBrowseButton().setDropdownEnabled(calculateDropDownButtonEnabled());
}
@Override
protected void setFieldEnabled(Control swtField, boolean enabled) {
if (m_editableSupport == null) {
m_editableSupport = new TextFieldEditableSupport(getSwtField());
}
m_editableSupport.setEditable(enabled);
}
protected void setIconIdFromScout(String s) {
boolean iconVisible = s != null;
boolean invalidateLayout = false;
if (m_browseButton.isVisible() != iconVisible) {
invalidateLayout = true;
}
m_browseButton.setVisible(iconVisible);
m_browseButton.setImage(getEnvironment().getIcon(s));
if (invalidateLayout && isConnectedToScout()) {
SwtLayoutUtility.invalidateLayout(m_browseButton);
}
}
@Override
protected void handleSwtFocusGained() {
super.handleSwtFocusGained();
if (m_proposalPopup == null) {
scheduleSelectAll();
}
if (getScoutObject().getErrorStatus() != null) {
requestProposalSupportFromSwt(getScoutObject().getDisplayText(), false);
}
}
@Override
protected void handleSwtFocusLost() {
super.handleSwtFocusLost();
if (!getSwtField().isDisposed()) {
getSwtField().setSelection(0, 0);
}
}
protected void scheduleSelectAll() {
getEnvironment().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (getSwtField().isDisposed()) {
return;
}
getSwtField().setSelection(0, getSwtField().getText().length());
}
});
}
protected void setProposalFormFromScout(ISmartFieldProposalForm form) {
synchronized (m_pendingProposalJobLock) {
if (m_pendingProposalJob != null) {
m_pendingProposalJob.cancel();
m_pendingProposalJob = null;
}
}
if (form != null) {
showProposalPopup(form);
}
else {
hideProposalPopup();
}
}
protected void showProposalPopup(final ISmartFieldProposalForm form) {
// close old
if (m_proposalPopup != null) {
if (m_proposalPopup.isVisible()) {
m_proposalPopup.closePart();
}
m_proposalPopup = null;
}
// show new
if (form != null) {
if (getSwtField().isFocusControl()) {
m_proposalPopup = new SwtScoutDropDownPopup(getEnvironment(), getSwtField(), getSwtField(), SWT.RESIZE);
m_proposalPopup.setMaxHeightHint(getScoutObject().getProposalFormHeight());
m_proposalPopup.addSwtScoutPartListener(new SwtScoutPartListener() {
@Override
public void partChanged(SwtScoutPartEvent e) {
switch (e.getType()) {
case SwtScoutPartEvent.TYPE_OPENING: {
notifyPopupEventListeners(IPopupSupportListener.TYPE_OPENING);
break;
}
case SwtScoutPartEvent.TYPE_CLOSING: {
hideProposalPopup();
break;
}
case SwtScoutPartEvent.TYPE_CLOSED: {
if (m_proposalPopup != null) {
m_proposalPopup = null;
notifyPopupEventListeners(IPopupSupportListener.TYPE_CLOSED);
}
break;
}
}
}
});
m_proposalPopup.getShell().addShellListener(new ShellAdapter() {
@Override
public void shellClosed(ShellEvent e) {
e.doit = false;
}
});
m_proposalPopup.makeNonFocusable();
try {
//add a listener whenever the form changes
m_proposalPopup.showForm(form);
form.addFormListener(new FormListener() {
@Override
public void formChanged(FormEvent e) throws ProcessingException {
switch (e.getType()) {
case FormEvent.TYPE_STRUCTURE_CHANGED:
Runnable job = new Runnable() {
@Override
public void run() {
if (m_proposalPopup != null) {
m_proposalPopup.autoAdjustBounds();
}
}
};
getEnvironment().invokeSwtLater(job);
break;
}
}
});
//enqueue a later display job since there may be waiting display tasks in the queue that change the table/tree
getSwtField().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (m_proposalPopup != null) {
m_proposalPopup.autoAdjustBounds();
}
}
});
}
catch (ProcessingException e1) {
LOG.error(e1.getMessage(), e1);
}
}
else {
Runnable t = new Runnable() {
@Override
public void run() {
try {
form.doClose();
}
catch (ProcessingException e) {
LOG.error("could not close smartfield-form", e);
}
}
};
getEnvironment().invokeScoutLater(t, 0);
}
}
}
protected void hideProposalPopup() {
synchronized (m_popupLock) {
if (m_proposalPopup != null && m_proposalPopup.isVisible()) {
m_proposalPopup.closePart();
}
m_proposalPopup = null;
}
}
protected void requestProposalSupportFromSwt(String text, boolean selectCurrentValue) {
synchronized (m_pendingProposalJobLock) {
if (m_pendingProposalJob == null) {
m_pendingProposalJob = new P_PendingProposalJob();
}
else {
m_pendingProposalJob.cancel();
}
m_pendingProposalJob.update(text, selectCurrentValue);
int delay = 200;
if (m_proposalPopup == null) {
delay = 0;
}
m_pendingProposalJob.schedule(delay);
}
}
private void acceptProposalFromSwt() {
synchronized (m_pendingProposalJobLock) {
if (m_pendingProposalJob != null) {
m_pendingProposalJob.cancel();
m_pendingProposalJob = null;
}
}
final String text = getSwtField().getText();
// notify Scout
Runnable t = new Runnable() {
@Override
public void run() {
getScoutObject().getUIFacade().setTextFromUI(text);
}
};
getEnvironment().invokeScoutLater(t, 0);
}
@Override
protected boolean filterKeyEvent(Event e) {
if (m_proposalPopup != null && m_proposalPopup.isVisible()) {
if (e.keyCode == SWT.ESC) {
hideProposalPopup();
return false;
}
else if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) {
acceptProposalFromSwt();
return false;
}
}
return super.filterKeyEvent(e);
}
@Override
protected boolean handleSwtInputVerifier() {
synchronized (m_pendingProposalJobLock) {
if (m_pendingProposalJob != null) {
m_pendingProposalJob.cancel();
m_pendingProposalJob = null;
}
}
final String text = getSwtField().getText();
final Holder<Boolean> result = new Holder<Boolean>(Boolean.class, true);
// notify Scout
Runnable t = new Runnable() {
@Override
public void run() {
boolean b = getScoutObject().getUIFacade().setTextFromUI(text);
result.setValue(b);
}
};
JobEx job = getEnvironment().invokeScoutLater(t, 0);
try {
job.join(2345);
}
catch (InterruptedException e) {
//nop
}
boolean processed = job.getState() == JobEx.NONE;
// end notify
getEnvironment().dispatchImmediateSwtJobs();
if (processed && (!result.getValue())) {
// keep focus
return false;
}
else {
// advance focus
return true;
}
}
protected void handleSwtBrowseAction() {
if (getSwtBrowseButton().isVisible() && getSwtBrowseButton().isEnabled()) {
requestProposalSupportFromSwt(ISmartField.BROWSE_ALL_TEXT, true);
}
}
@Override
protected void handleScoutPropertyChange(String name, Object newValue) {
super.handleScoutPropertyChange(name, newValue);
if (name.equals(ISmartField.PROP_ICON_ID)) {
setIconIdFromScout((String) newValue);
}
else if (name.equals(ISmartField.PROP_PROPOSAL_FORM)) {
setProposalFormFromScout((ISmartFieldProposalForm) newValue);
}
}
protected void handleScoutActionPropertyChange(String name, Object newValue) {
if (IAction.PROP_INHERIT_ACCESSIBILITY.equals(name) || IAction.PROP_EMPTY_SPACE.equals(name) ||
IAction.PROP_SINGLE_SELECTION.equals(name) || IAction.PROP_VISIBLE.equals(name)) {
handleDropDownButtonEnabled();
}
}
protected void handleDropDownButtonEnabled() {
getSwtBrowseButton().setDropdownEnabled(calculateDropDownButtonEnabled());
}
protected void handleTextModifiedFromUi(Event event) {
if (getUpdateSwtFromScoutLock().isAcquired()) {
return;
}
if (getSwtField().isVisible() && getSwtField().isFocusControl()) {
String text = getSwtField().getText();
if (text == null || text.length() == 0) {
// allow empty field without proposal
if (m_proposalPopup != null) {
requestProposalSupportFromSwt(text, false);
}
else {
// notify Scout
Runnable t = new Runnable() {
@Override
public void run() {
getScoutObject().getUIFacade().setTextFromUI(null);
}
};
getEnvironment().invokeScoutLater(t, 0);
// end notify
}
}
else {
requestProposalSupportFromSwt(text, false);
}
}
}
protected void handleKeyDownFromUI(Event event) {
switch (event.keyCode) {
case SWT.ARROW_DOWN:
case SWT.ARROW_UP:
case SWT.PAGE_DOWN:
case SWT.PAGE_UP:
if (getSwtField().isEnabled() && getSwtField().getEditable() && getSwtField().isVisible()) {
if (m_proposalPopup == null) {
requestProposalSupportFromSwt(ISmartField.BROWSE_ALL_TEXT, true);
}
else {
Widget c = null;
if (c == null) {
c = SwtUtility.findChildComponent(m_proposalPopup.getSwtContentPane(), Table.class);
}
if (c == null) {
c = SwtUtility.findChildComponent(m_proposalPopup.getSwtContentPane(), Tree.class);
}
SwtUtility.handleNavigationKey(c, event.keyCode);
}
}
break;
}
}
protected void handleTraverseFromUi(Event event) {
switch (event.keyCode) {
case SWT.ESC:
if (m_proposalPopup != null) {
event.doit = false;
}
break;
}
}
private void notifyPopupEventListeners(int eventType) {
IPopupSupportListener[] listeners;
synchronized (m_popupEventListenerLock) {
listeners = m_popupEventListeners.toArray(new IPopupSupportListener[m_popupEventListeners.size()]);
}
for (IPopupSupportListener listener : listeners) {
listener.handleEvent(eventType);
}
}
@Override
public void addPopupEventListener(IPopupSupportListener listener) {
synchronized (m_popupEventListenerLock) {
m_popupEventListeners.add(listener);
}
}
@Override
public void removePopupEventListener(IPopupSupportListener listener) {
synchronized (m_popupEventListenerLock) {
m_popupEventListeners.remove(listener);
}
}
private class P_UiFieldListener implements Listener {
@Override
public void handleEvent(Event event) {
switch (event.type) {
case SWT.Modify:
handleTextModifiedFromUi(event);
break;
case SWT.KeyDown:
handleKeyDownFromUI(event);
break;
case SWT.Traverse:
handleTraverseFromUi(event);
break;
}
}
}
private class P_PendingProposalJob extends JobEx implements Runnable {
private String m_text;
private boolean m_selectCurrentValue;
public P_PendingProposalJob() {
super("");
setSystem(true);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
getEnvironment().getDisplay().asyncExec(this);
return Status.OK_STATUS;
}
@Override
public void run() {
synchronized (m_pendingProposalJobLock) {
if (m_pendingProposalJob == this) {
m_pendingProposalJob = null;
}
else {
return;
}
}
if (getSwtField().isFocusControl()) {
Runnable t = new Runnable() {
@Override
public void run() {
getScoutObject().getUIFacade().openProposalFromUI(m_text, m_selectCurrentValue);
}
};
getEnvironment().invokeScoutLater(t, 0);
}
}
public void update(String text, boolean selectCurrentValue) {
m_text = text;
m_selectCurrentValue = selectCurrentValue;
}
}
private class P_ContextMenuListener extends MenuAdapter {
@Override
public void menuShown(MenuEvent e) {
for (MenuItem item : m_contextMenu.getItems()) {
disposeMenuItem(item);
}
final AtomicReference<IMenu[]> scoutMenusRef = new AtomicReference<IMenu[]>();
Runnable t = new Runnable() {
@Override
public void run() {
IMenu[] scoutMenus = getScoutObject().getUIFacade().firePopupFromUI();
scoutMenusRef.set(scoutMenus);
}
};
JobEx job = getEnvironment().invokeScoutLater(t, 1200);
try {
job.join(1200);
}
catch (InterruptedException ex) {
//nop
}
// grab the actions out of the job, when the actions are providden within
// the scheduled time the popup will be handled.
if (scoutMenusRef.get() != null) {
SwtMenuUtility.fillContextMenu(scoutMenusRef.get(), m_contextMenu, getEnvironment());
}
}
private void disposeMenuItem(MenuItem item) {
Menu menu = item.getMenu();
if (menu != null) {
for (MenuItem childItem : menu.getItems()) {
disposeMenuItem(childItem);
}
menu.dispose();
}
item.dispose();
}
} // end class P_ContextMenuListener
private class P_SwtBrowseButtonListener extends SelectionAdapter {
@Override
public void widgetSelected(SelectionEvent e) {
handleSwtBrowseAction();
}
} // end class P_SwtBrowseButtonListener
private class P_F2KeyStroke extends SwtKeyStroke {
public P_F2KeyStroke() {
super(SWT.F2);
}
@Override
public void handleSwtAction(Event e) {
handleSwtBrowseAction();
}
} // end class P_F2KeyStroke
private class P_ScoutActionPropertyChangeListener extends AbstractSwtScoutActionPropertyChangeListener {
@Override
public void propertyChange(final PropertyChangeEvent e) {
Runnable t = new Runnable() {
@Override
public void run() {
if (isHandleScoutPropertyChangeSwtThread()) {
try {
getUpdateSwtFromScoutLock().acquire();
//
handleScoutActionPropertyChange(e.getPropertyName(), e.getNewValue());
}
finally {
getUpdateSwtFromScoutLock().release();
}
}
}
};
getEnvironment().invokeSwtLater(t);
}
} // end class P_ScoutActionPropertyChangeListener
}