org.yccheok.jstock.gui.AutoCompleteJComboBox.java Source code

Java tutorial

Introduction

Here is the source code for org.yccheok.jstock.gui.AutoCompleteJComboBox.java

Source

/*
 * JStock - Free Stock Market Software
 * Copyright (C) 2015 Yan Cheng Cheok <yccheok@yahoo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.yccheok.jstock.gui;

import java.awt.Component;
import java.awt.Dimension;
import javax.swing.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.plaf.metal.MetalComboBoxEditor;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import org.yccheok.jstock.engine.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yccheok.jstock.engine.ResultSetType;
import org.yccheok.jstock.engine.ResultType;

/**
 *
 * @author yccheok
 */
public class AutoCompleteJComboBox extends JComboBox implements JComboBoxPopupAdjustable {

    // Use SubjectEx, in order to make notify method public.
    private static class SubjectEx<S, A> extends Subject<S, A> {
        @Override
        public void notify(S subject, A arg) {
            super.notify(subject, arg);
        }
    }

    /** Creates a new instance of AutoCompleteJComboBox */
    public AutoCompleteJComboBox() {
        this.stockInfoDatabase = null;

        // Save the offline mode renderer, so that we may reuse it when we
        // switch back to offline mode.
        this.oldListCellRenderer = this.getRenderer();

        this.changeMode(Mode.Offline);

        this.setEditable(true);

        this.keyAdapter = this.getEditorComponentKeyAdapter();

        // Use our own editor, in order to implement auto-complete feature.
        this.jComboBoxEditor = new MyJComboBoxEditor();
        this.setEditor(this.jComboBoxEditor);

        // Use to handle ENTER key pressed.
        this.getEditor().getEditorComponent().addKeyListener(this.keyAdapter);

        // Do not use keyAdapter to handle auto-complete feature, as it doesn't
        // handle IME input well. For example, you type "wm" and press "3",
        // keyAdapter will have no idea you are trying to choose the 3rd choice
        // provided by your IME. Instead, use documentListener, which will be
        // much more reliable.
        final Component component = this.getEditor().getEditorComponent();
        if (component instanceof JTextComponent) {
            ((JTextComponent) component).getDocument().addDocumentListener(this.getDocumentListener());
        } else {
            log.error("Unable to attach DocumentListener to AutoCompleteJComboBox.");
        }

        this.ajaxYahooSearchEngineMonitor.attach(getYahooMonitorObserver());
        this.ajaxGoogleSearchEngineMonitor.attach(getGoogleMonitorObserver());

        this.addActionListener(getActionListener());

        // Have a wide enough drop down list.
        this.addPopupMenuListener(this.getPopupMenuListener());

        // Create horizontal scroll bar if needed.
        // (I am not sure I still need this one as I already have adjustPopupWidth)
        adjustScrollBar();
    }

    private PopupMenuListener getPopupMenuListener() {
        return new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                // We will have a much wider drop down list.
                Utils.adjustPopupWidth(AutoCompleteJComboBox.this);
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                // Reset popup width.
                AutoCompleteJComboBox.this.setPopupWidth(-1);
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
                // Reset popup width.
                AutoCompleteJComboBox.this.setPopupWidth(-1);
            }
        };
    }

    private ActionListener getActionListener() {
        return new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                /* Handle mouse clicked. */
                if ((e.getModifiers()
                        & java.awt.event.InputEvent.BUTTON1_MASK) == java.awt.event.InputEvent.BUTTON1_MASK) {
                    final Object object = AutoCompleteJComboBox.this.getEditor().getItem();
                    /* Let us be extra paranoid. */
                    if (object instanceof DispType) {
                        DispType lastEnteredDisp = (DispType) object;
                        AutoCompleteJComboBox.this.dispSubject.notify(AutoCompleteJComboBox.this, lastEnteredDisp);
                    } else if (object instanceof StockInfo) {
                        // From our offline database.
                        StockInfo lastEnteredStockInfo = (StockInfo) object;
                        AutoCompleteJComboBox.this.stockInfoSubject.notify(AutoCompleteJComboBox.this,
                                lastEnteredStockInfo);
                    } else {
                        assert (false);
                    }

                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            // We schedule the below actions in GUI event queue,
                            // so that DocumentListener will not be triggered.
                            // But I am not sure why.
                            AutoCompleteJComboBox.this.getEditor().setItem(null);
                            AutoCompleteJComboBox.this.hidePopup();
                            AutoCompleteJComboBox.this.removeAllItems();
                        }
                    });
                }
            }
        };
    }

    /**
     * Assign a stock info database to this combo box.
     *
     * @param stockInfoDatabase the stock info database
     */
    public void setStockInfoDatabase(StockInfoDatabase stockInfoDatabase) {
        this.stockInfoDatabase = stockInfoDatabase;

        KeyListener[] listeners = this.getEditor().getEditorComponent().getKeyListeners();

        for (KeyListener listener : listeners) {
            if (listener == keyAdapter) {
                return;
            }
        }

        // Bug in Java 6. Most probably this listener had been removed during look n feel updating, reassign!
        this.getEditor().getEditorComponent().addKeyListener(keyAdapter);
        log.info("Reassign key adapter to combo box");
    }

    private DocumentListener getDocumentListener() {
        return new DocumentListener() {
            private volatile boolean ignore = false;

            @Override
            public void insertUpdate(DocumentEvent e) {
                try {
                    final String string = e.getDocument().getText(0, e.getDocument().getLength()).trim();
                    handle(string);
                } catch (BadLocationException ex) {
                    log.error(null, ex);
                }
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                try {
                    final String string = e.getDocument().getText(0, e.getDocument().getLength()).trim();
                    handle(string);
                } catch (BadLocationException ex) {
                    log.error(null, ex);
                }
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                try {
                    final String string = e.getDocument().getText(0, e.getDocument().getLength()).trim();
                    handle(string);
                } catch (BadLocationException ex) {
                    log.error(null, ex);
                }
            }

            private void _handle(final String string) {
                // We are no longer busy.
                busySubject.notify(AutoCompleteJComboBox.this, false);

                if (AutoCompleteJComboBox.this.getSelectedItem() != null) {
                    // Remember to use toString(). As getSelectedItem() can be
                    // either StockInfo, or ResultSet.
                    if (AutoCompleteJComboBox.this.getSelectedItem().toString().equals(string)) {
                        // We need to differentiate, whether "string" is from user
                        // typing, or drop down list selection. This is because when
                        // user perform selection, document change event will be triggered
                        // too. When string is from drop down list selection, user
                        // are not expecting any auto complete suggestion. Return early.
                        return;
                    }
                }

                if (string.isEmpty()) {
                    // Empty string. Return early. Do not perform hidePopup and
                    // removeAllItems right here. As when user performs list
                    // selection, previous text field item will be removed, and
                    // cause us fall into this scope. We do not want to hidePopup
                    // and removeAllItems when user is selecting his item.
                    //
                    // hidePopup and removeAllItems when user clears off all items
                    // in text field, will be performed through keyReleased.
                    return;
                }

                // Use to avoid endless DocumentEvent triggering.
                ignore = true;
                // During _handle operation, there will be a lot of ListDataListeners
                // trying to modify the content of our text field. We will not allow
                // them to do so.
                //
                // Without setReadOnly(true), when we type the first character "w", IME
                // will suggest ... However, when we call removeAllItems and addItem,
                // JComboBox will "commit" this suggestion to JComboBox's text field.
                // Hence, if we continue to type second character "m", the string displayed
                // at JComboBox's text field will be ...
                //
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(true);

                // Must hide popup. If not, the pop up windows will not be
                // resized.
                AutoCompleteJComboBox.this.hidePopup();
                AutoCompleteJComboBox.this.removeAllItems();

                boolean shouldShowPopup = false;

                if (AutoCompleteJComboBox.this.stockInfoDatabase != null) {
                    java.util.List<StockInfo> stockInfos = greedyEnabled
                            ? stockInfoDatabase.greedySearchStockInfos(string)
                            : stockInfoDatabase.searchStockInfos(string);

                    sortStockInfosIfPossible(stockInfos);

                    if (stockInfos.isEmpty() == false) {
                        // Change to offline mode before adding any item.
                        changeMode(Mode.Offline);
                    }

                    for (StockInfo stockInfo : stockInfos) {
                        AutoCompleteJComboBox.this.addItem(stockInfo);
                        shouldShowPopup = true;
                    }

                    if (shouldShowPopup) {
                        AutoCompleteJComboBox.this.showPopup();
                    } else {

                    } // if (shouldShowPopup)
                } // if (AutoCompleteJComboBox.this.stockInfoDatabase != null)

                if (shouldShowPopup == false) {
                    // OK. We found nothing from offline database. Let's
                    // ask help from online database.
                    // We are busy contacting server right now.

                    // TODO
                    // Only enable ajaxYahooSearchEngineMonitor, till we solve
                    // http://sourceforge.net/apps/mediawiki/jstock/index.php?title=TechnicalDisability
                    busySubject.notify(AutoCompleteJComboBox.this, true);

                    canRemoveAllItems = true;
                    ajaxYahooSearchEngineMonitor.clearAndPut(string);
                    ajaxGoogleSearchEngineMonitor.clearAndPut(string);
                }

                // When we are in windows look n feel, the text will always be selected. We do not want that.
                final Component component = AutoCompleteJComboBox.this.getEditor().getEditorComponent();
                if (component instanceof JTextField) {
                    JTextField jTextField = (JTextField) component;
                    jTextField.setSelectionStart(jTextField.getText().length());
                    jTextField.setSelectionEnd(jTextField.getText().length());
                    jTextField.setCaretPosition(jTextField.getText().length());
                }

                // Restore.
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(false);
                ignore = false;
            }

            private void handle(final String string) {
                if (ignore) {
                    return;
                }

                // Submit to GUI event queue. Used to avoid
                // Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Attempt to mutate in notification
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        _handle(string);
                    }
                });
            }
        };
    }

    // We should make this powerful combo box shared amoing different classes.
    private KeyAdapter getEditorComponentKeyAdapter() {

        return new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (KeyEvent.VK_ENTER == e.getKeyCode()) {
                    // We are no longer busy.
                    busySubject.notify(AutoCompleteJComboBox.this, false);

                    StockInfo lastEnteredStockInfo = null;
                    DispType lastEnteredDispType = null;

                    if (AutoCompleteJComboBox.this.getItemCount() > 0) {
                        int index = AutoCompleteJComboBox.this.getSelectedIndex();
                        if (index == -1) {
                            Object object = AutoCompleteJComboBox.this.getItemAt(0);
                            if (object instanceof StockInfo) {
                                lastEnteredStockInfo = (StockInfo) object;
                            } else if (object instanceof DispType) {
                                lastEnteredDispType = (DispType) object;
                            }
                        } else {
                            Object object = AutoCompleteJComboBox.this.getItemAt(index);
                            if (object instanceof StockInfo) {
                                lastEnteredStockInfo = (StockInfo) object;
                            } else if (object instanceof DispType) {
                                lastEnteredDispType = (DispType) object;
                            }
                        }
                    } else {
                        // If item count is 0, this means stockInfoDatabase
                        // unable provide us any result based on user query. I
                        // suspect we still need to below code, as 
                        // stockInfoDatabase will just return null. However, it
                        // should make no harm at this moment.
                        if (AutoCompleteJComboBox.this.stockInfoDatabase != null) {
                            final Object object = AutoCompleteJComboBox.this.getEditor().getItem();
                            if (object instanceof String) {
                                String lastEnteredString = ((String) object).trim();
                                lastEnteredStockInfo = AutoCompleteJComboBox.this.stockInfoDatabase
                                        .searchStockInfo(lastEnteredString);
                            } else {
                                assert (false);
                            }
                        }
                    }

                    AutoCompleteJComboBox.this.removeAllItems();
                    if (lastEnteredStockInfo != null) {
                        AutoCompleteJComboBox.this.stockInfoSubject.notify(AutoCompleteJComboBox.this,
                                lastEnteredStockInfo);
                    } else if (lastEnteredDispType != null) {
                        AutoCompleteJComboBox.this.dispSubject.notify(AutoCompleteJComboBox.this,
                                lastEnteredDispType);
                    } else {
                        // Do nothing.
                    }

                    return;
                } /* if(KeyEvent.VK_ENTER == e.getKeyCode()) */

                // If user removes item from text field, we will hidePopup and
                // removeAllItems. Please refer DocumentListener.handle, on why
                // don't we handle hidePopup and removeAllItems there.
                final Object object = AutoCompleteJComboBox.this.getEditor().getItem();
                if (object == null || object.toString().length() <= 0) {
                    AutoCompleteJComboBox.this.hidePopup();
                    AutoCompleteJComboBox.this.removeAllItems();
                }
            } /* public void keyReleased(KeyEvent e) */
        };
    }

    public boolean isGreedyEnabled() {
        return this.greedyEnabled;
    }

    public void setGreedyEnabled(boolean greedyEnabled, List<String> codeExtensionSortingOption) {
        this.greedyEnabled = greedyEnabled;
        this.codeExtensionSortingOption = codeExtensionSortingOption;
    }

    private void adjustScrollBar() {
        final int max_search = 8;
        // i < max_search is just a safe guard when getAccessibleChildrenCount
        // returns an arbitary large number. 8 is magic number
        JPopupMenu popup = null;
        for (int i = 0, count = this.getUI().getAccessibleChildrenCount(this); i < count && i < max_search; i++) {
            Object o = this.getUI().getAccessibleChild(this, i);
            if (o instanceof JPopupMenu) {
                popup = (JPopupMenu) o;
                break;
            }
        }
        if (popup == null) {
            return;
        }
        JScrollPane scrollPane = null;
        for (int i = 0, count = popup.getComponentCount(); i < count && i < max_search; i++) {
            Component c = popup.getComponent(i);
            if (c instanceof JScrollPane) {
                scrollPane = (JScrollPane) c;
                break;
            }
        }
        if (scrollPane == null) {
            return;
        }
        scrollPane.setHorizontalScrollBar(new JScrollBar(JScrollBar.HORIZONTAL));
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }

    // WARNING : If Java is having a major refactor on BasicComboBoxEditor class,
    // the following workaround will break. However, this is the best we can do
    // at this moment.
    private class MyJComboBoxEditor extends BasicComboBoxEditor {
        @Override
        protected JTextField createEditorComponent() {
            final MyTextField _editor = new MyTextField("");
            // Is there a better way to configure the correct UI for
            // JTextField?
            if (UIManager.getLookAndFeel().getClass().getName().equals("javax.swing.plaf.metal.MetalLookAndFeel")) {
                final Component component = new MetalComboBoxEditor().getEditorComponent();
                if (component instanceof JComponent) {
                    final JComponent jComponent = (JComponent) component;
                    _editor.setBorder(jComponent.getBorder());
                }
            } else {
                _editor.setBorder(null);
            }
            return _editor;
        }

        public void setReadOnly(boolean readonly) {
            this.readonly = readonly;
        }

        private class MyTextField extends JTextField {
            public MyTextField(String s) {
                super(s);
            }

            // workaround for 4530952
            @Override
            public void setText(String s) {
                if (readonly || getText().equals(s)) {
                    return;
                }
                super.setText(s);
            }
        }

        private boolean readonly = false;
    }

    /***************************************************************************
     * START OF ONLINE DATABASE FEATURE
     **************************************************************************/
    private enum Mode {
        Offline, // Suggestion will be getting through offline database.
        Online // Suggestion will be getting through online database.
    }

    private void changeMode(Mode mode) {
        ListCellRenderer me = null;

        if (mode == Mode.Offline) {
            if (this.getModel() instanceof SortedComboBoxModel) {
                this.setModel(new DefaultComboBoxModel());
            }

            // Check through JStockOptions, to determine which renderer to be
            // applied.
            if (JStock.instance().getJStockOptions()
                    .getStockInputSuggestionListOption() == JStockOptions.StockInputSuggestionListOption.OneColumn) {
                me = oldListCellRenderer;
            } else {
                assert (JStock.instance().getJStockOptions()
                        .getStockInputSuggestionListOption() == JStockOptions.StockInputSuggestionListOption.TwoColumns);
                me = offlineModeCellRenderer;
            }
        } else if (mode == Mode.Online) {
            if (!(this.getModel() instanceof SortedComboBoxModel)) {
                this.setModel(new SortedComboBoxModel());
            }

            me = onlineModeCellRenderer;
        }

        if (this.currentListCellRenderer != me) {
            this.currentListCellRenderer = me;
            this.setRenderer(this.currentListCellRenderer);
            // When we change mode, the previous inserted item(s) no longer valid.
            // Let's clear them up first, before we switch to a new renderer.
            this.removeAllItems();
        }
    }

    private Observer<AjaxGoogleSearchEngineMonitor, MatchSetType> getGoogleMonitorObserver() {
        return new Observer<AjaxGoogleSearchEngineMonitor, MatchSetType>() {

            @Override
            public void update(final AjaxGoogleSearchEngineMonitor subject, MatchSetType arg) {
                // Can we further enhance our search result?
                if (arg.Match.isEmpty()) {
                    StockInfo stockInfo = ajaxStockInfoSearchEngine.search(arg.Query);
                    if (stockInfo != null) {
                        MatchType matchType = new MatchType(stockInfo.code.toString().toUpperCase(),
                                stockInfo.symbol.toString(), null, null);
                        List<MatchType> matchTypes = new ArrayList<>();
                        matchTypes.add(matchType);
                        MatchSetType matchSetType = MatchSetType.newInstance(arg.Query, matchTypes);
                        // Overwrite!
                        arg = matchSetType;
                    }
                }

                final MatchSetType _arg = arg;

                if (SwingUtilities.isEventDispatchThread()) {
                    _update(subject, _arg);
                } else {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            _update(subject, _arg);
                        }
                    });
                }
            }

            public void _update(AjaxGoogleSearchEngineMonitor subject, MatchSetType arg) {
                final String string = AutoCompleteJComboBox.this.getEditor().getItem().toString().trim();
                if (string.isEmpty() || false == string.equalsIgnoreCase(arg.Query)) {
                    return;
                }

                // We are no longer busy.
                busySubject.notify(AutoCompleteJComboBox.this, false);

                // During _update operation, there will be a lot of ListDataListeners
                // trying to modify the content of our text field. We will not allow
                // them to do so.
                //
                // Without setReadOnly(true), when we type the first character "w", IME
                // will suggest ... However, when we call removeAllItems and addItem,
                // JComboBox will "commit" this suggestion to JComboBox's text field.
                // Hence, if we continue to type second character "m", the string displayed
                // at JComboBox's text field will be ...
                //
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(true);

                // Must hide popup. If not, the pop up windows will not be
                // resized. But this causes flickering. :(
                boolean isPopupHide = false;
                if (canRemoveAllItems) {
                    canRemoveAllItems = false;

                    isPopupHide = true;
                    AutoCompleteJComboBox.this.hidePopup();
                    AutoCompleteJComboBox.this.removeAllItems();

                    codes.clear();
                }

                if (arg.Match.isEmpty() == false) {
                    // Change to online mode before adding any item.
                    changeMode(Mode.Online);
                }

                for (MatchType match : arg.Match) {
                    if (codes.contains(match.getCode().toString())) {
                        continue;
                    }

                    if (!isPopupHide) {
                        isPopupHide = true;
                        AutoCompleteJComboBox.this.hidePopup();
                    }

                    codes.add(match.getCode().toString());
                    AutoCompleteJComboBox.this.addItem(match);
                }
                if (isPopupHide && AutoCompleteJComboBox.this.getItemCount() > 0) {
                    AutoCompleteJComboBox.this.showPopup();
                }

                // Restore.
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(false);
            }
        };
    }

    private Observer<AjaxYahooSearchEngineMonitor, ResultSetType> getYahooMonitorObserver() {
        return new Observer<AjaxYahooSearchEngineMonitor, ResultSetType>() {
            @Override
            public void update(final AjaxYahooSearchEngineMonitor subject, ResultSetType arg) {
                // Can we further enhance our search result?
                if (arg.Result.isEmpty()) {
                    StockInfo stockInfo = ajaxStockInfoSearchEngine.search(arg.Query);
                    if (stockInfo != null) {
                        ResultType resultType = new ResultType(stockInfo.code.toString().toUpperCase(),
                                stockInfo.symbol.toString());
                        List<ResultType> resultTypes = new ArrayList<>();
                        resultTypes.add(resultType);
                        // Overwrite!
                        arg = ResultSetType.newInstance(arg.Query, resultTypes);
                    }
                }

                final ResultSetType _arg = arg;

                if (SwingUtilities.isEventDispatchThread()) {
                    _update(subject, _arg);
                } else {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            _update(subject, _arg);
                        }
                    });
                }
            }

            public void _update(AjaxYahooSearchEngineMonitor subject, ResultSetType arg) {
                final String string = AutoCompleteJComboBox.this.getEditor().getItem().toString().trim();
                if (string.isEmpty() || false == string.equalsIgnoreCase(arg.Query)) {
                    return;
                }

                // We are no longer busy.
                busySubject.notify(AutoCompleteJComboBox.this, false);

                // During _update operation, there will be a lot of ListDataListeners
                // trying to modify the content of our text field. We will not allow
                // them to do so.
                //
                // Without setReadOnly(true), when we type the first character "w", IME
                // will suggest ... However, when we call removeAllItems and addItem,
                // JComboBox will "commit" this suggestion to JComboBox's text field.
                // Hence, if we continue to type second character "m", the string displayed
                // at JComboBox's text field will be ...
                //
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(true);

                // Must hide popup. If not, the pop up windows will not be
                // resized. But this causes flickering. :(
                boolean isPopupHide = false;
                if (canRemoveAllItems) {
                    canRemoveAllItems = false;

                    isPopupHide = true;
                    AutoCompleteJComboBox.this.hidePopup();
                    AutoCompleteJComboBox.this.removeAllItems();

                    codes.clear();
                }

                if (arg.Result.isEmpty() == false) {
                    // Change to online mode before adding any item.
                    changeMode(Mode.Online);
                }

                for (ResultType result : arg.Result) {
                    if (codes.contains(result.symbol)) {
                        continue;
                    }

                    if (!isPopupHide) {
                        isPopupHide = true;
                        AutoCompleteJComboBox.this.hidePopup();
                    }

                    codes.add(result.symbol);
                    AutoCompleteJComboBox.this.addItem(result);
                }
                if (isPopupHide && AutoCompleteJComboBox.this.getItemCount() > 0) {
                    AutoCompleteJComboBox.this.showPopup();
                }

                // Restore.
                AutoCompleteJComboBox.this.jComboBoxEditor.setReadOnly(false);
            }
        };
    }

    private final SubjectEx<AutoCompleteJComboBox, DispType> dispSubject = new SubjectEx<AutoCompleteJComboBox, DispType>();
    private final SubjectEx<AutoCompleteJComboBox, StockInfo> stockInfoSubject = new SubjectEx<AutoCompleteJComboBox, StockInfo>();
    private final SubjectEx<AutoCompleteJComboBox, Boolean> busySubject = new SubjectEx<AutoCompleteJComboBox, Boolean>();

    /**
     * Attach an observer to listen to stock info available event.
     *
     * @param observer an observer to listen to stock info available event
     */
    public void attachStockInfoObserver(Observer<AutoCompleteJComboBox, StockInfo> observer) {
        stockInfoSubject.attach(observer);
    }

    /**
     * Attach an observer to listen to ResultType available event.
     *
     * @param observer an observer to listen to ResultType available event
     */
    public void attachDispObserver(Observer<AutoCompleteJComboBox, DispType> observer) {
        dispSubject.attach(observer);
    }

    /**
     * Attach an observer to listen to busy state event.
     *
     * @param observer an observer to listen to busy state event
     */
    public void attachBusyObserver(Observer<AutoCompleteJComboBox, Boolean> observer) {
        busySubject.attach(observer);
    }

    /**
     * Removes all observers for this combo box.
     */
    public void dettachAll() {
        // For offline database feature.
        stockInfoSubject.dettachAll();
        // For online database feature.
        dispSubject.dettachAll();
        busySubject.dettachAll();
    }

    /**
     * Stop Ajax threading activity in this combo box. Once stop, this combo box
     * can no longer be reused.
     */
    public void stop() {
        ajaxYahooSearchEngineMonitor.stop();
        ajaxGoogleSearchEngineMonitor.stop();
    }

    private void sortStockInfosIfPossible(List<StockInfo> stockInfos) {
        if (!greedyEnabled) {
            return;
        }

        final Map<String, Integer> m = new HashMap<String, Integer>();
        for (int i = 0, ei = codeExtensionSortingOption.size(); i < ei; i++) {
            m.put(codeExtensionSortingOption.get(i), i);
        }

        Collections.sort(stockInfos, new Comparator<StockInfo>() {

            @Override
            public int compare(StockInfo o1, StockInfo o2) {
                String str1 = o1.code.toString();
                String str2 = o2.code.toString();
                String extension1 = null;
                String extension2 = null;
                int index1 = str1.lastIndexOf(".");
                int index2 = str2.lastIndexOf(".");
                if (index1 >= 0) {
                    extension1 = str1.substring(index1 + 1);
                }
                if (index2 >= 0) {
                    extension2 = str2.substring(index2 + 1);
                }

                Integer order1 = m.get(extension1);
                Integer order2 = m.get(extension2);

                if (Objects.equals(order1, order2)) {
                    return str1.compareTo(str2);
                }

                // With extension comes first.
                if (order1 != null && order2 == null) {
                    return -1;
                }

                if (order1 == null && order2 != null) {
                    return 1;
                }

                return order1 - order2;
            }
        });
    }

    private boolean greedyEnabled = false;
    private List<String> codeExtensionSortingOption = java.util.Collections.emptyList();

    private final ListCellRenderer offlineModeCellRenderer = new StockInfoCellRenderer();
    private final ListCellRenderer onlineModeCellRenderer = new DispTypeCellRenderer();
    private final ListCellRenderer oldListCellRenderer;
    private ListCellRenderer currentListCellRenderer;

    // Online database.
    private final AjaxYahooSearchEngineMonitor ajaxYahooSearchEngineMonitor = new AjaxYahooSearchEngineMonitor();
    private final AjaxGoogleSearchEngineMonitor ajaxGoogleSearchEngineMonitor = new AjaxGoogleSearchEngineMonitor();
    private final AjaxStockInfoSearchEngine ajaxStockInfoSearchEngine = new AjaxStockInfoSearchEngine();

    /***************************************************************************
     * END OF ONLINE DATABASE FEATURE
     **************************************************************************/

    // Offline database.
    private StockInfoDatabase stockInfoDatabase;
    private final KeyAdapter keyAdapter;
    private final MyJComboBoxEditor jComboBoxEditor;

    private static final Log log = LogFactory.getLog(AutoCompleteJComboBox.class);

    /*
     * =========================================================================
     * START >>
     * 
     * Hacking code picked from
     * http://javabyexample.wisdomplug.com/java-concepts/34-core-java/59-tips-and-tricks-for-jtree-jlist-and-jcombobox-part-i.html
     *
     */

    /**
     * Set the combo box popup width.
     * 
     * @param popupWidth the combo box popup width
     */
    @Override
    public void setPopupWidth(int popupWidth) {
        this.popupWidth = popupWidth;
    }

    /**
    * Override to handle the popup Size.
    */
    @Override
    public void doLayout() {
        try {
            layingOut = true;
            super.doLayout();
        } finally {
            layingOut = false;
        }
    }

    /**
    * Overriden to handle the popup Size
    */
    @Override
    public Dimension getSize() {
        Dimension dim = super.getSize();
        if (!layingOut && popupWidth != 0) {
            // Ensure the popup size must be a least equal or larger than combo
            // box size.
            if (dim.width < popupWidth) {
                dim.width = popupWidth;
            }
        }
        return dim;
    }

    /**
     * Set the popup Width.
     */
    private int popupWidth = 0;

    /**
     * Keep track of whether layout is happening.
     */
    private boolean layingOut = false;

    private volatile boolean canRemoveAllItems = false;
    private final Set<String> codes = new HashSet<>();

    /*
     * 
     * Hacking code picked from
     * http://javabyexample.wisdomplug.com/java-concepts/34-core-java/59-tips-and-tricks-for-jtree-jlist-and-jcombobox-part-i.html
     * 
     * << END
     * =========================================================================
     */
}