/* * Copyright (c) 2010, Michael Grossmann, Nikolaus Moll * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the jo-widgets.org nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.jowidgets.spi.impl.swt.common.widgets; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.jowidgets.common.mask.TextMaskMode; import org.jowidgets.common.types.InputChangeEventPolicy; import org.jowidgets.common.types.Markup; import org.jowidgets.common.verify.IInputVerifier; import org.jowidgets.common.widgets.controller.IFocusListener; import org.jowidgets.common.widgets.controller.IKeyListener; import org.jowidgets.spi.impl.controller.FocusObservable; import org.jowidgets.spi.impl.controller.KeyObservable; import org.jowidgets.spi.impl.mask.TextMaskVerifierFactory; import org.jowidgets.spi.impl.swt.common.image.SwtImageRegistry; import org.jowidgets.spi.impl.swt.common.options.SwtOptions; import org.jowidgets.spi.impl.swt.common.util.FontProvider; import org.jowidgets.spi.impl.swt.common.widgets.event.LazyKeyEventContentFactory; import org.jowidgets.spi.impl.verify.InputVerifierHelper; import org.jowidgets.spi.widgets.IComboBoxSelectionSpi; import org.jowidgets.spi.widgets.IComboBoxSpi; import org.jowidgets.spi.widgets.setup.IComboBoxSelectionSetupSpi; import org.jowidgets.spi.widgets.setup.IComboBoxSetupSpi; import org.jowidgets.util.Assert; import org.jowidgets.util.event.IObservableCallback; public class ComboBoxImpl extends AbstractInputControl implements IComboBoxSelectionSpi, IComboBoxSpi { public static final int KEY_CODE_DELETE = 0x7F; /* ASCII DEL */ public static final int KEY_CODE_BACK_SPACE = '\b'; private final boolean isAutoCompletionMode; private final boolean isSelectionMode; private final KeyListener keyListener; private final KeyObservable keyObservable; private final FocusObservable focusObservable; private boolean isPopupVisible; private boolean programmaticTextChange; public ComboBoxImpl( final Object parentUiReference, final IComboBoxSelectionSetupSpi setup, final SwtImageRegistry imageRegistry) { super(createCombo((Composite) parentUiReference, setup), imageRegistry); this.programmaticTextChange = true; this.isAutoCompletionMode = setup.isAutoCompletion(); this.isSelectionMode = !(setup instanceof IComboBoxSetupSpi); this.isPopupVisible = false; setElements(setup.getElements()); if (isAutoCompletionMode) { getUiReference().addVerifyListener(new VerifyListener() { @Override public void verifyText(final VerifyEvent event) { if (programmaticTextChange) { return; } programmaticTextChange = true; final String newEnteredText; int pos = event.start; if (event.keyCode != KEY_CODE_DELETE && event.keyCode != KEY_CODE_BACK_SPACE) { newEnteredText = event.text; pos = pos + event.text.length(); } else { final Point selection = getUiReference().getSelection(); if (event.keyCode == KEY_CODE_BACK_SPACE && selection.x != selection.y) { pos = Math.max(0, pos - 1); } newEnteredText = ""; } final String previousText = getUiReference().getText(); //linux impl of swt has event.end = -1 in some cases final int startIndex = Math.max(0, event.start); final int endIndex = Math.max(0, event.end); final String newText = previousText.substring(0, startIndex) + newEnteredText + previousText.substring(endIndex); if (!previousText.equals(newText)) { event.doit = doAutoCompletion(newText, event.keyCode, pos); if (isSelectionMode) { final int selectedIndex = getSelectedIndex(); if (selectedIndex != -1) { final String[] elements = getElements(); if (elements != null && selectedIndex < elements.length) { fireInputChanged(elements[selectedIndex], selectedIndex); } } } } programmaticTextChange = false; } }); } getUiReference().addSelectionListener(new SelectionListener() { @Override public void widgetSelected(final SelectionEvent e) { if (!programmaticTextChange) { fireInputChanged(getUiReference().getText(), getSelectedIndex()); } } @Override public void widgetDefaultSelected(final SelectionEvent e) { if (!programmaticTextChange) { fireInputChanged(getUiReference().getText(), getSelectedIndex()); } } }); if (setup.getInputChangeEventPolicy() == InputChangeEventPolicy.ANY_CHANGE) { if (!isSelectionMode || isAutoCompletionMode) { getUiReference().addModifyListener(new ModifyListener() { @Override public void modifyText(final ModifyEvent e) { if (!programmaticTextChange) { fireInputChanged(getUiReference().getText(), getSelectedIndex()); } } }); } } else if (setup.getInputChangeEventPolicy() == InputChangeEventPolicy.EDIT_FINISHED) { getUiReference().addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { fireInputChanged(getUiReference().getText(), getSelectedIndex()); } }); } else { throw new IllegalArgumentException("InputChangeEventPolicy '" + setup.getInputChangeEventPolicy() + "' is not known."); } if (!isSelectionMode) { final IComboBoxSetupSpi comboBoxSetup = (IComboBoxSetupSpi) setup; if (SwtOptions.hasInputVerification()) { final IInputVerifier maskVerifier = TextMaskVerifierFactory.create(this, comboBoxSetup.getMask()); final IInputVerifier inputVerifier = InputVerifierHelper.getInputVerifier(maskVerifier, comboBoxSetup); if (inputVerifier != null) { this.getUiReference().addVerifyListener(new VerifyListener() { @Override public void verifyText(final VerifyEvent verifyEvent) { verifyEvent.doit = inputVerifier.verify( getUiReference().getText(), verifyEvent.text, verifyEvent.start, verifyEvent.end); } }); } } if (comboBoxSetup.getMaxLength() != null) { getUiReference().setTextLimit(comboBoxSetup.getMaxLength().intValue()); } if (SwtOptions.hasInputVerification() && comboBoxSetup.getMask() != null && TextMaskMode.FULL_MASK == comboBoxSetup.getMask().getMode()) { setText(comboBoxSetup.getMask().getPlaceholder()); } } else { getUiReference().addTraverseListener(new TraverseListener() { @Override public void keyTraversed(final TraverseEvent e) { if (e.detail == SWT.TRAVERSE_ESCAPE || e.detail == SWT.TRAVERSE_RETURN) { e.doit = false; } } }); } if (SwtOptions.hasComboTruncateWorkaround() && (isAutoCompletionMode || !isSelectionMode)) { getUiReference().addListener(SWT.Resize, new ResizeTruncateWorkaroundListener()); } this.keyListener = new KeyListener() { @Override public void keyReleased(final KeyEvent e) { keyObservable.fireKeyReleased(new LazyKeyEventContentFactory(e)); } @Override public void keyPressed(final KeyEvent e) { keyObservable.fireKeyPressed(new LazyKeyEventContentFactory(e)); if ((e.stateMask & SWT.ALT) == SWT.ALT && (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN)) { isPopupVisible = true; } if (e.keyCode == SWT.ESC || e.keyCode == SWT.CR) { isPopupVisible = false; } } }; final IObservableCallback keyObservableCallback = new IObservableCallback() { private boolean listenerAdded = false; @Override public void onLastUnregistered() { if (listenerAdded) { getUiReference().removeKeyListener(keyListener); listenerAdded = false; } } @Override public void onFirstRegistered() { if (!listenerAdded) { getUiReference().addKeyListener(keyListener); listenerAdded = true; } } }; this.keyObservable = new KeyObservable(keyObservableCallback); this.focusObservable = new FocusObservable(); getUiReference().addFocusListener(new FocusListener() { @Override public void focusLost(final FocusEvent e) { isPopupVisible = false; focusObservable.focusLost(); } @Override public void focusGained(final FocusEvent e) { focusObservable.focusGained(); } }); programmaticTextChange = false; } @Override public void addKeyListener(final IKeyListener listener) { keyObservable.addKeyListener(listener); } @Override public void removeKeyListener(final IKeyListener listener) { keyObservable.removeKeyListener(listener); } @Override public void addFocusListener(final IFocusListener listener) { focusObservable.addFocusListener(listener); } @Override public void removeFocusListener(final IFocusListener listener) { focusObservable.removeFocusListener(listener); } private static Combo createCombo(final Composite parent, final IComboBoxSelectionSetupSpi setup) { final boolean isAutoCompletionMode = setup.isAutoCompletion(); final boolean isSelectionMode = !(setup instanceof IComboBoxSetupSpi); final boolean hasEditor = isAutoCompletionMode || !isSelectionMode; final int flags; if (hasEditor) { flags = SWT.NONE | SWT.DROP_DOWN; } else { flags = SWT.NONE | SWT.READ_ONLY; } return new Combo(parent, flags); } private static int getMaxTextLength(final String[] elements) { int max = 1; if (elements != null) { for (final String item : elements) { max = Math.max(max, item.length()); } } return max; } @Override public Combo getUiReference() { return (Combo) super.getUiReference(); } @Override public void setEditable(final boolean editable) { getUiReference().setEnabled(editable); } @Override public String[] getElements() { return getUiReference().getItems(); } @Override public void setElements(final String[] elements) { Assert.paramNotNull(elements, "elements"); programmaticTextChange = true; getUiReference().setItems(elements); if (isAutoCompletionMode && isSelectionMode && elements.length > 0) { getUiReference().setTextLimit(getMaxTextLength(elements)); } programmaticTextChange = false; } @Override public int getSelectedIndex() { return getUiReference().getSelectionIndex(); } @Override public void setSelectedIndex(final int index) { programmaticTextChange = true; if (index > -1) { getUiReference().select(index); } else { getUiReference().deselect(getUiReference().getSelectionIndex()); } programmaticTextChange = false; if (!getUiReference().isFocusControl()) { fireInputChanged(getUiReference().getText(), index); } } @Override public String getText() { return getUiReference().getText(); } @Override public void setText(final String text) { programmaticTextChange = true; getUiReference().setText(text); programmaticTextChange = false; if (!getUiReference().isFocusControl()) { fireInputChanged(text, getSelectedIndex()); } } @Override public void setFontSize(final int size) { getUiReference().setFont(FontProvider.deriveFont(getUiReference().getFont(), size)); } @Override public void setFontName(final String fontName) { getUiReference().setFont(FontProvider.deriveFont(getUiReference().getFont(), fontName)); } @Override public void setMarkup(final Markup markup) { getUiReference().setFont(FontProvider.deriveFont(getUiReference().getFont(), markup)); } @Override public void setSelection(final int start, final int end) { getUiReference().setSelection(new Point(start, end)); } @Override public void select() { getUiReference().setSelection(new Point(0, getUiReference().getText().length())); } @Override public void setCaretPosition(final int pos) { setSelection(pos, pos); } @Override public int getCaretPosition() { return getUiReference().getSelection().y; } @Override public void setPopupVisible(final boolean visible) { this.isPopupVisible = true; getUiReference().setListVisible(visible); } @Override public boolean isPopupVisible() { return isPopupVisible; //do not use is popup visible because it will be false if closing key events arrive, //so key observer can not determine if key event will open or closed the popup //return getUiReference().getListVisible(); } private void fireInputChanged(final String text, final int index) { fireInputChanged(new Selection(index, text)); } private boolean doAutoCompletion(final String text, final int keyCode, final int pos) { if (!isSelectionMode && (keyCode == KEY_CODE_DELETE || keyCode == KEY_CODE_BACK_SPACE)) { return true; } final CompletionItem ci = getIndexOfPrefix(text); if (isSelectionMode && !ci.isPrefix && (keyCode == KEY_CODE_DELETE || keyCode == KEY_CODE_BACK_SPACE)) { getUiReference().setSelection(new Point(pos, getUiReference().getText().length())); return false; } if (ci.isPrefix) { getUiReference().select(ci.index); // Only open the popup list in selection due to windows behavior: If the list is visible, // Windows automatically selects an item, if the text partially matches an element if (isSelectionMode && keyCode != 0 && !getUiReference().getListVisible()) { getUiReference().setListVisible(true); isPopupVisible = true; } getUiReference().setSelection(new Point(pos, ci.completion.length())); return false; } return !isSelectionMode || ci.isPrefix; } private CompletionItem getIndexOfPrefix(final String text) { String completion = ""; boolean isPrefix = false; final String lowerText = text.toLowerCase(); final boolean isEmpty = lowerText.length() == 0; final String[] items = getUiReference().getItems(); int index = 0; if (isEmpty) { for (final String item : items) { if (item.length() == 0) { isPrefix = true; completion = item; break; } index++; } } else { int prefixIndex = -1; for (final String item : items) { if (item.toLowerCase().equals(lowerText)) { prefixIndex = -1; isPrefix = true; completion = item; break; } if (prefixIndex < 0 && item.toLowerCase().startsWith(lowerText)) { prefixIndex = index; isPrefix = true; completion = item; } index++; } if (prefixIndex >= 0) { index = prefixIndex; } } return new CompletionItem(index, isPrefix, completion); } private static final class CompletionItem { private final int index; private final boolean isPrefix; private final String completion; private CompletionItem(final int index, final boolean isPrefix, final String completion) { this.index = index; this.isPrefix = isPrefix; this.completion = completion; } } private final class ResizeTruncateWorkaroundListener implements Listener { @Override public void handleEvent(final Event event) { getUiReference().setSelection(new Point(0, 0)); } }; private final class Selection { private final int index; private final String text; private Selection(final int index, final String text) { this.index = index; this.text = text; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + index; result = prime * result + ((text == null) ? 0 : text.hashCode()); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Selection other = (Selection) obj; if (index != other.index) { return false; } if (text == null) { if (other.text != null) { return false; } } else if (!text.equals(other.text)) { return false; } return true; } } }