content: /*****************************************************************
   *   Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *    https://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   ****************************************************************/
  
  package org.apache.cayenne.modeler.util.combo;
  
  import javax.swing.JComboBox;
  import javax.swing.JList;
  import javax.swing.SwingUtilities;
  import javax.swing.text.JTextComponent;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.awt.event.KeyEvent;
  import java.awt.event.KeyListener;
  
  /**
   * AutoCompletion class handles user input and suggests matching variants (see CAY-911)
   *
   */
  public class AutoCompletion implements FocusListener, KeyListener, Runnable {
      /**
       * Property to mark combobox as 'auto-completing'
       */
      public static final String AUTOCOMPLETION_PROPERTY = "JComboBox.autoCompletion";
      
      /**
       * A list with matching items
       */
      private final SuggestionList suggestionList; 
  
      /**
       * Combo with auto-completion
       */
      private final JComboBox comboBox;
      
      private final JTextComponent textEditor;
      
      private final boolean allowsUserValues;
      
      protected AutoCompletion(final JComboBox comboBox, boolean strict, boolean allowsUserValues) {
          this.comboBox = comboBox;
          textEditor = ((JTextComponent)comboBox.getEditor().getEditorComponent());
          this.allowsUserValues = allowsUserValues;
          suggestionList = new SuggestionList(comboBox, strict);
  
          // Marking combobox as auto-completing
          comboBox.putClientProperty(AUTOCOMPLETION_PROPERTY, Boolean.TRUE);
      }
  
      /**
       * Enables auto-completion for specified combobox
       *
       * @param comboBox Combo to be featured
       * @param strict Whether strict matching (check 'startWith' or 'contains') should be used
       * @param allowsUserValues Whether non-present items are allowed 
       */
      public static void enable(JComboBox comboBox, boolean strict, boolean allowsUserValues) {
          comboBox.setEditable(true);
          KeyListener[] listeners = comboBox.getEditor().getEditorComponent().getListeners(KeyListener.class);
          comboBox.setEditor(new CustomTypeComboBoxEditor(comboBox, allowsUserValues));
          for (KeyListener listener : listeners) {
              comboBox.getEditor().getEditorComponent().addKeyListener(listener);
          }
  
  
          AutoCompletion ac = new AutoCompletion(comboBox, strict, allowsUserValues);
          comboBox.addFocusListener(ac);
          ac.textEditor.addKeyListener(ac);
  
          //original keys would not work properly
          SwingUtilities.replaceUIActionMap(comboBox, null);
      }
      
      /**
       * Enables auto-completion for specified combobox
       */
      public static void enable(JComboBox comboBox) {
          enable(comboBox, true, false);
      }
  
      public void focusGained(FocusEvent e) {}
  
      public void focusLost(FocusEvent e) {
          suggestionList.hide();
      }
  
      public void keyPressed(KeyEvent e) {
          handleKeyPressed(e);
      }
  
      public void keyReleased(KeyEvent e) {
          if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE || e.getKeyCode() == KeyEvent.VK_ENTER) {
              String text = textEditor.getText();
              if (comboBox.isShowing()) {
                  suggestionList.hide();
                  suggestionList.filter(text);
                  suggestionList.show();
              }
          }
      }
  
      public void keyTyped(KeyEvent e) {}
  
      public void run() {
          String text = textEditor.getText();
  
          //need to hide first because Swing incorrectly updates popups (getSize() returns
          //dimension not the same as seen on the screen)
          suggestionList.hide();
  
          if (comboBox.isShowing()) {
              suggestionList.filter(text);
  
              if (suggestionList.getItemCount() > 0) {
                  suggestionList.show();
              }
          }
      }
      
      /**
       * Calculates next selection row, according to a pressed key and selects it.
       * This might affect either suggestion list or original popup
       */
      private void handleKeyPressed(KeyEvent e) {
          boolean suggest = suggestionList.isVisible();
  
          if (suggest) {
              processKeyPressedWhenSuggestionListIsVisible(e);
          } else {
              processKeyPressedWhenSuggestionListIsInvisible(e);
          }
  
          //scroll doesn't work in suggestionList..so we will scroll manually
          suggestionListScrolling();
      }
  
      private void processKeyPressedWhenSuggestionListIsInvisible(KeyEvent e){
          int sel = comboBox.getSelectedIndex();
          int max = comboBox.getItemCount() - 1;
  
          int next;
          switch (e.getKeyCode()) {
              case KeyEvent.VK_UP:
              case KeyEvent.VK_NUMPAD8:
                  next = sel - 1;
                  break;
              case KeyEvent.VK_DOWN:
              case KeyEvent.VK_NUMPAD2:
                  next = sel + 1;
                  break;
              case KeyEvent.VK_PAGE_UP:
                  next = sel - comboBox.getMaximumRowCount();
                  break;
              case KeyEvent.VK_PAGE_DOWN:
                  next = sel + comboBox.getMaximumRowCount();
                  break;
              case KeyEvent.VK_HOME:
                  next = 0;
                  break;
              case KeyEvent.VK_END:
                  next = max;
                  break;
              case KeyEvent.VK_ENTER:
              case KeyEvent.VK_ESCAPE:
                  return;
              default:
                  //invoke in end of AWT thread so that information in textEditor would update
                  SwingUtilities.invokeLater(this);
                  return;
          }
          e.consume();
          handleNavigationKeys(false,next,sel,max);
      }
  
      private void processKeyPressedWhenSuggestionListIsVisible(KeyEvent e){
          int sel = suggestionList.getSelectedIndex();
          int max = suggestionList.getItemCount() - 1;
          int next;
          switch (e.getKeyCode()) {
              case KeyEvent.VK_UP:
              case KeyEvent.VK_NUMPAD8:
                  next = sel - 1;
                  break;
              case KeyEvent.VK_DOWN:
              case KeyEvent.VK_NUMPAD2:
                  next = sel + 1;
                  break;
              case KeyEvent.VK_PAGE_UP:
                  next = sel - comboBox.getMaximumRowCount();
                  break;
              case KeyEvent.VK_PAGE_DOWN:
                  next = sel + comboBox.getMaximumRowCount();
                  break;
              case KeyEvent.VK_HOME:
                  next = 0;
                  break;
              case KeyEvent.VK_END:
                  next = max;
                  break;
              case KeyEvent.VK_ENTER:
                  processEnterPressed();
                  return;
              case KeyEvent.VK_ESCAPE:
                  suggestionList.hide();
                  return;
              default:
                  //invoke in end of AWT thread so that information in textEditor would update
                  SwingUtilities.invokeLater(this);
                  return;
          }
          e.consume();
          handleNavigationKeys(true,next,sel,max);
      }
  
      private void processEnterPressed(){
          Object value = suggestionList.getSelectedValue();
          if (!allowsUserValues && value == null && suggestionList.getItemCount() > 0) {
              value = suggestionList.getItemAt(0);
          }
          //reset the item (value == null) only if user values are not supported
          if (value != null || !allowsUserValues) {
              comboBox.setSelectedItem(value);
          }
          suggestionList.hide();
      }
  
      private void handleNavigationKeys(boolean suggest, int next, int sel, int max){
          if (!suggest && !comboBox.isPopupVisible()) {
              comboBox.setPopupVisible(true);
              return;
          }
  
          if (comboBox.getItemCount() > 0) {
              if (next < 0) {
                  next = 0;
              }
  
              if (next > max) {
                  next = max;
              }
  
              if (next != sel) {
                  if (suggest) {
                      suggestionList.setSelectedIndex(next);
                  } else {
                      comboBox.setPopupVisible(true);
                      comboBox.setSelectedIndex(next);
                  }
              }
              textEditor.requestFocus();
          }
      }
  
      private void suggestionListScrolling(){
          JList list = suggestionList.getList();
          int selectedIndex = suggestionList.getSelectedIndex();
          list.ensureIndexIsVisible(selectedIndex);
      }
  }
  
  