content: /*
   * Copyright 2008-2013 Andre Pfeiler
   *
   * This file is part of FindBugs-IDEA.
   *
   * FindBugs-IDEA 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 3 of the License, or
   * (at your option) any later version.
   *
   * FindBugs-IDEA 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 FindBugs-IDEA.  If not, see <http://www.gnu.org/licenses/>.
   */
  package org.twodividedbyzero.idea.findbugs.gui.tree.view;
  
  import com.intellij.openapi.diagnostic.Logger;
  import com.intellij.openapi.wm.IdeFocusManager;
  import com.intellij.ui.JBColor;
  import com.intellij.ui.components.JBList;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  import org.twodividedbyzero.idea.findbugs.common.util.GuiUtil;
  import org.twodividedbyzero.idea.findbugs.gui.common.AbstractBar;
  import org.twodividedbyzero.idea.findbugs.gui.common.ScrollPaneFacade;
  import org.twodividedbyzero.idea.findbugs.gui.common.ToolBarButton;
  import org.twodividedbyzero.idea.findbugs.resources.GuiResources;
  
  import javax.swing.BorderFactory;
  import javax.swing.DefaultListModel;
  import javax.swing.Icon;
  import javax.swing.JComponent;
  import javax.swing.JLabel;
  import javax.swing.JLayeredPane;
  import javax.swing.JPanel;
  import javax.swing.JRootPane;
  import javax.swing.JScrollPane;
  import javax.swing.JTextField;
  import javax.swing.ListSelectionModel;
  import javax.swing.SwingConstants;
  import javax.swing.SwingUtilities;
  import javax.swing.Timer;
  import javax.swing.UIManager;
  import javax.swing.event.DocumentEvent;
  import javax.swing.event.DocumentListener;
  import javax.swing.event.ListSelectionEvent;
  import javax.swing.event.ListSelectionListener;
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.IllegalComponentStateException;
  import java.awt.Point;
  import java.awt.Rectangle;
  import java.awt.Toolkit;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.ComponentAdapter;
  import java.awt.event.ComponentEvent;
  import java.awt.event.ComponentListener;
  import java.awt.event.FocusAdapter;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.awt.event.InputEvent;
  import java.awt.event.KeyAdapter;
  import java.awt.event.KeyEvent;
  import java.awt.event.KeyListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseMotionAdapter;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Stack;
  import java.util.regex.Pattern;
  import java.util.regex.PatternSyntaxException;
  
  
  /**
   * $Date$
   *
   * @author Andre Pfeiler<andrep@twodividedbyzero.org>
   * @version $Revision$
   * @since 0.9.29-dev
   */
  @SuppressWarnings({"HardCodedStringLiteral", "MagicCharacter", "AnonymousInnerClass", "AnonymousInnerClass"})
  @edu.umd.cs.findbugs.annotations.SuppressFBWarnings({"SE_BAD_FIELD_INNER_CLASS"})
  abstract class QuickSearch<E> {
  
  	private static final Logger LOGGER = Logger.getInstance(QuickSearch.class.getName());
  
  	private int _searchDelay = 500;
  	private boolean _searchActivated;
  	private JLayeredPane _layerPane;
  	private SearchPopup _popup;
  	private RecentSearchesPopup _recentSearchesPopup;
  	private JComponent _owner;
  	private JComponent _component;
  	private Color _noMatchForeground;
  	private KeyListener _keyListener;
  	private FocusListener _focusListener;
  	private ComponentListener _componentListener;
  	private int _cursor = -1;
  	private Color _foregroundColor = JBColor.BLACK;
  	private Color _backgroundColor = new JBColor(new Color(255, 255, 200), JBColor.LIGHT_GRAY);
  	private final Stack<String> _recentSearches;
  
  
  	QuickSearch() {
  		_recentSearches = new Stack<String>();
  	}
  
  
  	final void installImpl(@NotNull final JComponent component) {
  		_component = component;
  		_owner = component;
  		installListeners();
  	}
  
  
  	private void installListeners() {
  		if (_componentListener == null) {
  			_componentListener = createComponentListener();
  		}
  		getComponent().addComponentListener(_componentListener);
  
  		if (_keyListener == null) {
  			_keyListener = createKeyListener();
  		}
  		getComponent().addKeyListener(_keyListener);
  
  		if (_focusListener == null) {
  			_focusListener = createFocusListener();
  		}
  		getComponent().addFocusListener(_focusListener);
  	}
  
  
  	abstract void uninstallListeners();
  
  
  	abstract int getElementCount();
  
  
  	abstract String convertElementToString(final E element);
  
  
  	abstract List<?> getElementsCache();
  
  
  	@Nullable
  	abstract E getElementAt(int index);
  
  
  	abstract void setSelectedElement(final int index);
  
  
  	private SearchPopup createSearchPopup(final String searchText) {
  		return new SearchPopup(searchText);
  	}
  
  
  	private KeyListener createKeyListener() {
  		return new KeyAdapter() {
  			@Override
  			public void keyTyped(final KeyEvent e) {
  				keyTypedOrPressed(e);
  			}
  
  
  			@Override
  			public void keyPressed(final KeyEvent e) {
  				keyTypedOrPressed(e);
  			}
  		};
  	}
  
  
  	private FocusListener createFocusListener() {
  		return new FocusAdapter() {
  			@Override
  			public void focusGained(final FocusEvent e) {
  				_searchActivated = true;
  			}
  
  
  			@Override
  			public void focusLost(final FocusEvent focusevent) {
  				hidePopup();
  			}
  		};
  	}
  
  
  	private ComponentListener createComponentListener() {
  		return new ComponentAdapter() {
  			@Override
  			public void componentHidden(final ComponentEvent e) {
  				super.componentHidden(e);
  				hidePopup();
  			}
  
  
  			@Override
  			public void componentResized(final ComponentEvent e) {
  				super.componentResized(e);
  				relocateAndResize();
  			}
  
  
  			@Override
  			public void componentMoved(final ComponentEvent e) {
  				super.componentMoved(e);
  				relocateAndResize();
  			}
  		};
  	}
  
  
  	private void keyTypedOrPressed(@NotNull final KeyEvent e) {
  		if ((isActivationKey(e) || _searchActivated) && !isDeactivationKey(e)) {
  			String searchingText = "";
  			if (e.getID() == KeyEvent.KEY_TYPED) {
  				if ((e.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) != 0) { // alt mask
  					return;
  				}
  				if (e.isAltDown()) {
  					return;
  				}
  
  				searchingText = String.valueOf(e.getKeyChar());
  			}
  
  			if (_popup == null) {
  				showPopup(searchingText);
  			}
  
  			if (e.getKeyCode() != KeyEvent.VK_ENTER) {
  				_popup.processKeyEvent(e);
  				e.consume();
  			}
  		} else if (isDeactivationKey(e)) {
  			hidePopup();
  			IdeFocusManager.findInstance().doWhenFocusSettlesDown(new Runnable() {
  				@Override
  				public void run() {
  					IdeFocusManager.findInstance().requestFocus(_owner, true);
  				}
  			});
  		}
  	}
  
  
  	final void showPopup(final String text) {
  		hidePopup();
  		_searchActivated = true;
  		final JComponent component = (JComponent) GuiUtil.getScrollPane(_component);
  		if (component != null) {
  			_component = component;
  		}
  
  		final JRootPane rootPane = SwingUtilities.getRootPane(component);
  		if (rootPane != null) {
  			_layerPane = rootPane.getLayeredPane();
  			_popup = createSearchPopup(text);
  			_layerPane.add(_popup, JLayeredPane.POPUP_LAYER);
  			relocateAndResize();
  			_popup.setVisible(true);
  			_popup.validate();
  
  			if (!_recentSearches.isEmpty()) {
  				showRecentSearchesPopup();
  			} else {
  				initRecentSearchesPopup(false);
  			}
  		} else {
  			//noinspection AssignmentToNull
  			_layerPane = null;
  		}
  
  	}
  
  
  	@SuppressWarnings({"AssignmentToNull"})
  	protected void hidePopup() {
  		_searchActivated = false;
  		if (_layerPane != null && _popup != null) {
  			hideRecentSearchesPopup();
  			_layerPane.remove(_popup);
  			_layerPane.validate();
  			_layerPane.repaint();
  			_layerPane = null;
  			_popup = null;
  			setCursor(-1);
  		}
  	}
  
  
  	private Point relocateAndResize() {
  		if (_popup != null) {
  			final Dimension popupSize;
  			final Point componentLocation;
  			try {
  				popupSize = _popup.getPreferredSize();
  				componentLocation = _component.getLocationOnScreen();
  				SwingUtilities.convertPointFromScreen(componentLocation, _layerPane);
  				componentLocation.y -= popupSize.height;
  				if (componentLocation.y < 0) {
  					componentLocation.y = 0;
  				}
  			} catch (final IllegalComponentStateException ignore) {
  				return null;
  			}
  
  			_popup.setLocation(componentLocation);
  			_popup.setSize(popupSize);
  
  			return componentLocation;
  		} else {
  			return null;
  		}
  	}
  
  
  	private static boolean isNavigationKey(final KeyEvent e) {
  		return isFindNextOccurrenceKey(e) || isFindPreviousOccurrenceKey(e);
  	}
  
  
  	private static boolean isFindNextOccurrenceKey(final KeyEvent e) {
  		return e.getKeyCode() == KeyEvent.VK_F3;
  	}
  
  
  	private static boolean isFindPreviousOccurrenceKey(final KeyEvent e) {
  		return e.getKeyCode() == KeyEvent.VK_F3 && e.getModifiersEx() == InputEvent.SHIFT_DOWN_MASK;
  	}
  
  
  	private static boolean isActivationKey(final KeyEvent e) {
  		final char keyChar = e.getKeyChar();
  		return e.getID() == KeyEvent.KEY_TYPED && e.getKeyCode() != KeyEvent.VK_F4 && e.getModifiersEx() != InputEvent.ALT_MASK && e.getModifiersEx() != InputEvent.ALT_DOWN_MASK && (Character.isLetterOrDigit(keyChar) || keyChar == '*' || keyChar == '?');
  	}
  
  
  	private static boolean isDeactivationKey(final KeyEvent e) {
  		final int keyCode = e.getKeyCode();
  		return keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_PAGE_UP || keyCode == KeyEvent.VK_PAGE_DOWN || keyCode == KeyEvent.VK_HOME || keyCode == KeyEvent.VK_END || keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN;
  	}
  
  
  	protected int getSearchDelay() {
  		return _searchDelay;
  	}
  
  
  	protected void setSearchDelay(final int searchDelay) {
  		_searchDelay = searchDelay;
  	}
  
  
  	protected Component getComponent() {
  		return _component;
  	}
  
  
  	protected boolean isPopupVisible() {
  		return _popup != null;
  	}
  
  
  	protected int getCursor() {
  		return _cursor;
  	}
  
  
  	protected void setCursor(final int cursor) {
  		_cursor = cursor;
  	}
  
  
  	protected Color getNoMatchForeground() {
  		if (_noMatchForeground == null) {
  			return JBColor.RED;
  		} else {
  			return _noMatchForeground;
  		}
  	}
  
  
  	@SuppressWarnings("UnusedDeclaration")
  	protected Color getForegroundColor() {
  		return _foregroundColor;
  	}
  
  
  	@SuppressWarnings("UnusedDeclaration")
  	protected void setForegroundColor(final Color foregroundColor) {
  		_foregroundColor = foregroundColor;
  	}
  
  
  	@SuppressWarnings("UnusedDeclaration")
  	protected Color getBackgroundColor() {
  		return _backgroundColor;
  	}
  
  
  	@SuppressWarnings("UnusedDeclaration")
  	protected void setBackgroundColor(final Color backgroundColor) {
  		_backgroundColor = backgroundColor;
  	}
  
  
  	private int find(final String text) {
  		final int count = getElementCount();
  		if (count == 0) {
  			return -1;
  		}
  
  		// find from cursor
  		int cursor = getCursor();
  		if (cursor == -1 && count > 0) {
  			cursor = 0;
  			setCursor(cursor);
  		}
  
  		for (int i = cursor; i < count; i++) {
  			final E element = getElementAt(i);
  			if (compare(element, text)) {
  				addToRecentSearches(text);
  				return i;
  			}
  		}
  
  		// if not found, search from begin to cursor
  		for (int i = 0; i < cursor; i++) {
  			final E element = getElementAt(i);
  			if (compare(element, text)) {
  				//setCursor(i);
  				addToRecentSearches(text);
  				return i;
  			}
  		}
  
  		return -1;
  
  	}
  
  
  	private int findNextOccurrence(final String text) {
  		final int count = getElementCount();
  		if (count == 0) {
  			return !text.isEmpty() ? -1 : 0;
  		}
  
  		for (int i = getCursor() + 1; i < count; i++) {
  			final E element = getElementAt(i);
  			if (compare(element, text)) {
  				return i;
  			}
  		}
  
  		return -1;
  	}
  
  
  	private int findPreviousOccurrence(final String text) {
  		final int count = getElementCount();
  		if (count == 0) {
  			return !text.isEmpty() ? -1 : 0;
  		}
  
  		for (int i = getCursor() - 1; i >= 0; i--) {
  			final E element = getElementAt(i);
  			if (compare(element, text)) {
  				return i;
  			}
  		}
  
  		return -1;
  	}
  
  
  	protected boolean compare(final E element, final String searchText) {
  		if (searchText == null || searchText.trim().isEmpty()) {
  			return true;
  		}
  
  		final String text = convertElementToString(element);
  		if (text == null) {
  			return false;
  		}
  
  		try {
  			//_pattern = Pattern.compile(isFromStart() ? "^" + s : s, isCaseSensitive() ? 0 : Pattern.CASE_INSENSITIVE);
  			Pattern pattern = Pattern.compile('^' + searchText + '^', 0);
  			boolean found = pattern.matcher(text).find();
  			if (found) {
  				return found;
  			}
  
  			pattern = Pattern.compile('^' + searchText, 0);
  			found = pattern.matcher(text).find();
  			if (found) {
  				return found;
  			}
  
  			pattern = Pattern.compile('^' + searchText, Pattern.CASE_INSENSITIVE);
  			found = pattern.matcher(text).find();
  			if (found) {
  				return found;
  			}
  
  			pattern = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
  			found = pattern.matcher(text).find();
  			return found;
  		} catch (final PatternSyntaxException ignore) {
  			return false;
  		}
  	}
  
  
  	private class SearchField extends JTextField {
  
  		SearchField() {
  		}
  
  
  		@Override
  		public Dimension getPreferredSize() {
  			final Dimension size = super.getPreferredSize();
  			size.width = getFontMetrics(getFont()).stringWidth(getText()) + 4;
  			return size;
  		}
  
  
  		@Override
  		public void processKeyEvent(final KeyEvent e) {
  
  			final int keyCode = e.getKeyCode();
  			if (keyCode == KeyEvent.VK_BACK_SPACE && getDocument().getLength() == 0) {
  				e.consume();
  				return;
  			}
  
  			final boolean isNavigationKey = isNavigationKey(e);
  
  			if (isDeactivationKey(e) && !isNavigationKey) {
  				hidePopup();
  				e.consume();
  				return;
  			}
  
  			if (isNavigationKey) {
  				e.consume();
  			}
  
  			super.processKeyEvent(e);
  		}
  	}
  
  
  	private class SearchPopup extends JPanel {
  
  		private JLabel _label;
  		private SearchField _searchField;
  		private NavigationToolBar _toolBar;
  
  
  		private SearchPopup(final String searchText) {
  			initGui(searchText);
  		}
  
  
  		private void initGui(final String text) {
  			//addKeyListener(_keyListener);
  
  			_label = new JLabel("Search for: ");
  			//noinspection MagicNumber
  			_label.setFont(new Font(getFont().getName(), Font.BOLD, 12));
  			_label.setForeground(_foregroundColor);
  			_label.setVerticalAlignment(SwingConstants.BOTTOM);
  
  			_toolBar = new NavigationToolBar("test", false, SwingConstants.HORIZONTAL);
  			_toolBar.setForeground(_foregroundColor);
  			_toolBar.setVisible(false);
  
  			_searchField = new SearchField();
  			_searchField.setForeground(_foregroundColor);
  			_searchField.setFocusable(false);
  			_searchField.setOpaque(false);
  			_searchField.setBorder(BorderFactory.createEmptyBorder());
  			_searchField.setCursor(getCursor());
  
  			_searchField.getDocument().addDocumentListener(new DocumentListener() {
  				private final Timer _timer = new Timer(200, new ActionListener() {
  					public void actionPerformed(final ActionEvent e) {
  						doFind();
  					}
  				});
  
  
  				public void insertUpdate(final DocumentEvent e) {
  					startTimer();
  				}
  
  
  				public void removeUpdate(final DocumentEvent e) {
  					startTimer();
  				}
  
  
  				public void changedUpdate(final DocumentEvent e) {
  					startTimer();
  				}
  
  
  				void doFind() {
  					final String text = _searchField.getText().trim();
  					if (!text.isEmpty()) {
  						final int found = find(text);
  						if (found == -1) {
  							_noMatchForeground = getNoMatchForeground();
  							_searchField.setForeground(_noMatchForeground);
  							_toolBar.setVisible(false);
  							remove(_toolBar);
  							updatePopupBounds();
  						} else {
  							_searchField.setForeground(_foregroundColor);
  							_toolBar.setVisible(true);
  							add(_toolBar, BorderLayout.LINE_END);
  							QuickSearch.this.setCursor(found);
  							setSelectedElement(found);
  							updatePopupBounds();
  						}
  					} else {
  						hidePopup();
  					}
  				}
  
  
  				void startTimer() {
  					updatePopupBounds();
  					if (getSearchDelay() > 0) {
  						_timer.setInitialDelay(getSearchDelay());
  						if (_timer.isRunning()) {
  							_timer.restart();
  						} else {
  							_timer.setRepeats(false);
  							_timer.start();
  						}
  					} else {
  						doFind();
  					}
  				}
  
  			});
  
  			_searchField.setText(text);
  
  			setBackground(_backgroundColor);
  			setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(JBColor.GRAY, 1), BorderFactory.createEmptyBorder(1, 6, 1, 6)));
  			setLayout(new BorderLayout(2, 0));
  			/*final Dimension size = _label.getPreferredSize();
  			size.height = _searchField.getPreferredSize().height;
  			_label.setPreferredSize(size);
  
  			final Dimension toolBarSize = _toolBar.getPreferredSize();
  			toolBarSize.height = _searchField.getPreferredSize().height;*/
  
  			final Dimension size = _label.getPreferredSize();
  			size.height = _toolBar.getPreferredSize().height;
  			_label.setPreferredSize(size);
  
  			final Dimension sizeField = _searchField.getPreferredSize();
  			sizeField.height = size.height;
  			_searchField.setPreferredSize(sizeField);
  
  			add(_label, BorderLayout.LINE_START);
  			add(_searchField, BorderLayout.CENTER);
  		}
  
  
  		private void updatePopupBounds() {
  			if (_popup != null) {
  				_searchField.invalidate();
  				try {
  					final Dimension size = _searchField.getPreferredSize();
  					size.width += _label.getPreferredSize().width;
  					size.width += _toolBar.isVisible() ? _toolBar.getPreferredSize().width : 0;
  					size.width += 20 /*new JLabel(_searchField.getText()).getPreferredSize().width*//* + 19*/;
  					size.height = _popup.getSize().height;
  					_popup.setSize(size);
  					_popup.setPreferredSize(size);
  					_popup.validate();
  				} catch (final Exception e) {
  					LOGGER.debug(e);
  				}
  			}
  		}
  
  
  		@Override
  		public void processKeyEvent(final KeyEvent e) {
  			_searchField.processKeyEvent(e);
  			if (e.isConsumed()) {
  				final String text = getSearchText();
  
  				if (text.isEmpty()) {
  					return;
  				}
  
  				int found = -1;
  				boolean foundPrev = true;
  				boolean foundNext = true;
  
  				if (isFindNextOccurrenceKey(e) && !isFindPreviousOccurrenceKey(e)) {
  					found = findNextOccurrence(text);
  					foundNext = found > 1;
  				} else if (isFindPreviousOccurrenceKey(e)) {
  					found = findPreviousOccurrence(text);
  					foundPrev = found > -1;
  				}
  
  				_toolBar.setPrevEnabled(foundPrev);
  				_toolBar.setNextEnabled(foundNext);
  
  				if (found == -1) {
  					//_searchField.setForeground(getNoMatchForeground());
  				} else {
  					_searchField.setForeground(UIManager.getColor("Toolip.foreground"));
  					QuickSearch.this.setCursor(found);
  					setSelectedElement(found);
  				}
  			}
  			if (e.getKeyCode() != KeyEvent.VK_ENTER) {
  				e.consume();
  			}
  			super.processKeyEvent(e);
  		}
  
  
  		public String getSearchText() {
  			return _searchField.getText();
  		}
  
  
  		public SearchField getSearchField() {
  			return _searchField;
  		}
  
  
  		public NavigationToolBar getToolbar() {
  			return _toolBar;
  		}
  
  
  		@Override
  		public String toString() {
  			final StringBuilder sb = new StringBuilder();
  			sb.append("SearchPopup");
  			sb.append("{_label=").append(_label);
  			sb.append(", _searchField=").append(_searchField);
  			sb.append(", _toolBar=").append(_toolBar);
  			sb.append('}');
  			return sb.toString();
  		}
  	}
  
  
  	private class NavigationToolBar extends AbstractBar {
  
  		private final Icon _prevIcon = GuiResources.NAVIGATION_MOVEUP_ICON;
  		private final Icon _nextIcon = GuiResources.NAVIGATION_MOVEDOWN_ICON;
  		private ToolBarButton _prevButton;
  		private ToolBarButton _nextButton;
  
  
  		private NavigationToolBar(final String name, final boolean floatable, final int orientation) {
  			super(name);
  			setFloatable(floatable);
  			setOrientation(orientation);
  			setRollover(false);
  			initGui();
  		}
  
  
  		private void initGui() {
  			setOpaque(false);
  			setFocusable(false);
  			setRollover(false);
  			setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
  
  			final AbstractBar.AbstractComponentAction prevAction = new AbstractComponentAction("Previous Occurrence", "Previous Occurrence (Ctrl+F3)", KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK, KeyEvent.VK_F3) {
  				private static final long serialVersionUID = 0L;
  
  
  				public void actionPerformed(final ActionEvent e) {
  					_popup.processKeyEvent(new KeyEvent(_prevButton, 401, System.currentTimeMillis(), InputEvent.SHIFT_DOWN_MASK, KeyEvent.VK_F3, KeyEvent.CHAR_UNDEFINED));
  				}
  			};
  			_prevButton = new ToolBarButton(prevAction, _prevIcon);
  			add(_prevButton);
  
  
  			final AbstractBar.AbstractComponentAction nextAction = new AbstractComponentAction("Next Occurrence", "Next Occurrence (F3)", KeyEvent.VK_F3, 0, KeyEvent.VK_F3) {
  				private static final long serialVersionUID = 0L;
  
  
  				public void actionPerformed(final ActionEvent e) {
  					_popup.processKeyEvent(new KeyEvent(_nextButton, 401, System.currentTimeMillis(), 0, KeyEvent.VK_F3, KeyEvent.CHAR_UNDEFINED));
  				}
  			};
  			_nextButton = new ToolBarButton(nextAction, _nextIcon);
  			add(_nextButton);
  		}
  
  
  		public void setPrevEnabled(final boolean enable) {
  			_prevButton.setEnabled(enable);
  		}
  
  
  		public void setNextEnabled(final boolean enable) {
  			_nextButton.setEnabled(enable);
  		}
  
  
  		@Override
  		public String toString() {
  			final StringBuilder sb = new StringBuilder();
  			sb.append("NavigationToolBar");
  			sb.append("{_prevIcon=").append(_prevIcon);
  			sb.append(", _nextIcon=").append(_nextIcon);
  			sb.append(", _prevButton=").append(_prevButton);
  			sb.append(", _nextButton=").append(_nextButton);
  			sb.append('}');
  			return sb.toString();
  		}
  	}
  
  
  	private class RecentSearchesPopup extends JPanel {
  
  		private JLabel _label;
  		private DefaultListModel _listModel;
  		private JBList _list;
  		private int _mouseOverIndex;
  
  
  		private RecentSearchesPopup() {
  			initGui();
  		}
  
  
  		private void initGui() {
  			_label = new JLabel("Recent Searches");
  			_label.setPreferredSize(new Dimension(getPreferredSize().width, _label.getPreferredSize().height + 5));
  			_label.setForeground(_foregroundColor);
  			_label.setBackground(_backgroundColor);
  			_label.setOpaque(true);
  			_label.setHorizontalAlignment(SwingConstants.CENTER);
  			_label.setVerticalAlignment(SwingConstants.CENTER);
  
  			_listModel = new DefaultListModel();
  			_list = new JBList(_listModel);
  			_list.setBorder(BorderFactory.createEmptyBorder(8, 5, 8, 5));
  			_list.setAutoscrolls(true);
  			_list.setFocusable(false);
  			_list.setRequestFocusEnabled(false);
  
  			updateListData(_recentSearches);
  			_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  
  			_list.addMouseMotionListener(new MouseMotionAdapter() {
  				@Override
  				public void mouseMoved(final MouseEvent e) {
  					locationToListIndex(e);
  				}
  			});
  
  			_list.addListSelectionListener(new ListSelectionListener() {
  				public void valueChanged(final ListSelectionEvent e) {
  					if (e.getValueIsAdjusting()) {
  						return;
  					}
  
  					if (_list.getSelectedIndex() > -1) {
  						_popup.getSearchField().setText((String) _list.getSelectedValue());
  					}
  				}
  			});
  
  			//updatePopupBounds();
  			final JScrollPane scrollPane = ScrollPaneFacade.createScrollPane();
  			scrollPane.getViewport().setView(_list);
  			scrollPane.setBorder(null);
  			scrollPane.setForeground(_foregroundColor);
  
  			setLayout(new BorderLayout());
  			setBackground(_backgroundColor);
  			setOpaque(true);
  			setBorder(BorderFactory.createLineBorder(JBColor.GRAY, 1));
  
  			add(_label, BorderLayout.NORTH);
  			add(scrollPane, BorderLayout.CENTER);
  		}
  
  
  		private void locationToListIndex(final MouseEvent e) {
  			final Point p = e.getPoint();
  			int overIndex = _list.locationToIndex(p);
  
  			final Rectangle itemRect = _list.getCellBounds(overIndex, overIndex);
  			if (!itemRect.contains(p)) {
  				overIndex = -1;
  			}
  
  			if (overIndex != _mouseOverIndex) {
  				_list.setSelectedIndex(overIndex);
  				_list.scrollRectToVisible(itemRect);
  				_mouseOverIndex = overIndex;
  			}
  		}
  
  
  		private void scrollIndexToVisible(final int index) {
  			final Rectangle rect = _list.getCellBounds(index, index);
  			_list.setSelectedIndex(index);
  			_list.scrollRectToVisible(rect);
  		}
  
  
  		@Override
  		public Dimension getPreferredSize() {
  			return new Dimension(_label.getPreferredSize().width + 60, 200);
  		}
  
  
  		public String getLongestString() {
  			int maxLen = 0;
  			String longest = null;
  
  			for (int i = 0; i < _listModel.size(); i++) {
  				final String s = (String) _listModel.elementAt(i);
  				if (s.length() > maxLen) {
  					maxLen = s.length();
  					longest = s;
  				}
  			}
  			return longest;
  		}
  
  
  		void updateListData(final Stack<String> stack) {
  			_listModel.removeAllElements();
  			final ListIterator<String> iter = stack.listIterator(stack.size());
  			while (iter.hasPrevious()) {
  				_listModel.addElement(iter.previous());
  			}
  		}
  
  
  		@Nullable
  		private JBList getList() {
  			return _list;
  		}
  
  
  		@Override
  		public String toString() {
  			final StringBuilder sb = new StringBuilder();
  			sb.append("RecentSearchesPopup");
  			sb.append("{_label=").append(_label);
  			sb.append(", _listModel=").append(_listModel);
  			sb.append(", _list=").append(_list);
  			sb.append(", _mouseOverIndex=").append(_mouseOverIndex);
  			sb.append('}');
  			return sb.toString();
  		}
  	}
  
  
  	private void showRecentSearchesPopup() {
  		final JComponent component = (JComponent) GuiUtil.getScrollPane(_component);
  		if (component != null) {
  			_component = component;
  		}
  
  		final JRootPane rootPane = SwingUtilities.getRootPane(component);
  		if (rootPane != null) {
  			_layerPane = rootPane.getLayeredPane();
  			initRecentSearchesPopup(true);
  		}
  
  	}
  
  
  	private void initRecentSearchesPopup(final boolean visible) {
  		_recentSearchesPopup = createRecentSearchesPopup();
  		_layerPane.add(_recentSearchesPopup, JLayeredPane.POPUP_LAYER);
  		relocateAndResizeRecentSearches();
  		_recentSearchesPopup.setVisible(visible);
  		_recentSearchesPopup.validate();
  	}
  
  
  	protected void hideRecentSearchesPopup() {
  		if (_recentSearchesPopup != null) {
  			//final Rectangle bounds = _popup.getBounds();
  			_layerPane.remove(_recentSearchesPopup);
  			//_layerPane.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
  			_layerPane.validate();
  			_layerPane.repaint();
  			//_layerPane = null;
  			//noinspection AssignmentToNull
  			_recentSearchesPopup = null;
  			setCursor(-1);
  		}
  	}
  
  
  	private Point relocateAndResizeRecentSearches() {
  		if (_recentSearchesPopup != null && _popup != null) {
  			final Dimension popupSize;
  			final Point componentLocation;
  			try {
  				popupSize = _recentSearchesPopup.getPreferredSize();
  				componentLocation = _popup.getLocationOnScreen();
  				SwingUtilities.convertPointFromScreen(componentLocation, _layerPane);
  				componentLocation.y -= popupSize.height + 2;
  				if (componentLocation.y < 0) {
  					componentLocation.y = 2;
  				}
  			} catch (final IllegalComponentStateException ignore) {
  				return null;
  			}
  
  			_recentSearchesPopup.setLocation(componentLocation);
  			_recentSearchesPopup.setSize(popupSize);
  
  			return componentLocation;
  		} else {
  			return null;
  		}
  	}
  
  
  	protected RecentSearchesPopup createRecentSearchesPopup() {
  		return new RecentSearchesPopup();
  	}
  
  
  	private void addToRecentSearches(final String text) {
  		if (text.length() >= 3) {
  			if (_recentSearches.size() >= 30) {
  				_recentSearches.remove(_recentSearches.size() - 1);
  			}
  
  			if (_recentSearchesPopup != null) {
  				if (!_recentSearches.contains(text)) {
  					_recentSearches.push(text);
  					//Collections.reverse(_recentSearches);
  					_recentSearchesPopup.updateListData(_recentSearches);
  				}
  
  				final JBList jList = _recentSearchesPopup.getList();
  				if (jList != null) {
  					final DefaultListModel listModel = (DefaultListModel) jList.getModel();
  					final int index = listModel.indexOf(text);
  					jList.setSelectedIndex(index);
  					_recentSearchesPopup.scrollIndexToVisible(index);
  				}
  			}
  
  		}
  	}
  
  
  	public String toString() {
  		final StringBuilder sb = new StringBuilder();
  		sb.append("QuickSearch");
  		sb.append("{_searchDelay=").append(_searchDelay);
  		sb.append(", _searchActivated=").append(_searchActivated);
  		sb.append(", _layerPane=").append(_layerPane);
  		sb.append(", _popup=").append(_popup);
  		sb.append(", _recentSearchesPopup=").append(_recentSearchesPopup);
  		sb.append(", _component=").append(_component);
  		sb.append(", _noMatchForeground=").append(_noMatchForeground);
  		sb.append(", _keyListener=").append(_keyListener);
  		sb.append(", _focusListener=").append(_focusListener);
  		sb.append(", _componentListener=").append(_componentListener);
  		sb.append(", _cursor=").append(_cursor);
  		sb.append(", _foregroundColor=").append(_foregroundColor);
  		sb.append(", _backgroundColor=").append(_backgroundColor);
  		sb.append(", _recentSearches=").append(_recentSearches);
  		sb.append('}');
  		return sb.toString();
  	}
  }
  