2017-08-01 3 views
1

Je souhaite implémenter une fonctionnalité de saisie semi-automatique. Actuellement, j'ai un JPanel contenant JTextField et, lorsque l'utilisateur commence à taper, une saisie semi-automatique (JPopupMenu) apparaît, contenant plusieurs options. Le problème est qu'il prend le focus à partir du champ de texte et que l'utilisateur ne peut plus taper. Lorsque je reviens sur le champ de texte, l'utilisateur n'a plus de navigation entre les options (en utilisant les boutons haut et bas). Avoir aussi l'accent sur le menu ne me permet pas d'intercepter son KeyListener (je ne sais pas pourquoi), et quand j'essaie de traiter l'entrée sur le champ de texte, j'ai des problèmes avec les éléments de menu.Implémentation de la saisie semi-automatique avec jtextfield et jpopupmenu

Alors que je veux avoir:

  1. Un menu contextuel avec des options qui change de manière dynamique lorsque l'utilisateur modifie le texte TextField, ayant menu actif
  2. encore L'utilisateur peut naviguer entre les options à l'aide et les touches fléchées , ainsi que les touches Entrée et Échap pour utiliser l'option ou fermer la fenêtre contextuelle respectivement.

Est-il possible de traiter les événements de clavier dans le menu et de renvoyer les événements de saisie au champ de texte?

Quelle est la bonne façon d'aborder mon problème?

Voici le code ci-dessous. Merci d'avance!

import javax.swing.*; 
import java.awt.*; 
import java.awt.event.KeyEvent; 
import java.awt.event.KeyListener; 


class TagVisual extends JPanel { 

    private JTextField editField; 

    public TagVisual() { 

     FlowLayout layout = new FlowLayout(); 
     layout.setHgap(0); 
     layout.setVgap(0); 
     setLayout(layout); 

     editField = new JTextField(); 
     editField.setBackground(Color.RED); 

     editField.setPreferredSize(new Dimension(200, 20)); 

     editField.addKeyListener(new KeyListener() { 
      @Override 
      public void keyTyped(KeyEvent e) { 
       JPopupMenu menu = new JPopupMenu(); 
       menu.add("Item 1"); 
       menu.add("Item 2"); 
       menu.add("Item 3"); 
       menu.addKeyListener(new KeyListener() { 
        @Override 
        public void keyTyped(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyTyped"); 
        } 

        @Override 
        public void keyPressed(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyPressed"); 
        } 

        @Override 
        public void keyReleased(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyReleased"); 
        } 
       }); 
       menu.show(editField, 0, getHeight()); 
      } 

      @Override 
      public void keyPressed(KeyEvent e) { 

      } 

      @Override 
      public void keyReleased(KeyEvent e) { 

      } 
     }); 

     add(editField, FlowLayout.LEFT); 
    } 

    public void place(JPanel panel) { 
     panel.add(this); 

     editField.grabFocus(); 
    } 
} 

public class MainWindow { 

    private JPanel mainPanel; 
    private JFrame frame; 

    public MainWindow(JFrame frame) { 

     mainPanel = new JPanel(new FlowLayout()); 
     TagVisual v = new TagVisual(); 
     v.place(mainPanel); 

     this.frame = frame; 
    } 

    public static void main(String[] args) { 
     JFrame frame = new JFrame("TextFieldPopupIssue"); 

     frame.setContentPane(new MainWindow(frame).mainPanel); 
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
     frame.pack(); 
     frame.setVisible(true); 
    } 
} 
+0

Ou utilisez une bibliothèque tierce. Voir par exemple https://stackoverflow.com/q/14186955/1076463 – Robin

Répondre

0

La solution la plus simple fait menu ne focalisable:

menu.setFocusable(false); 

et de gérer les clés dans l'éditeur

editField.addKeyListener(new KeyAdapter() { 
      @Override 
      public void keyPressed(KeyEvent e) { 
       if(KeyEvent.VK_DOWN == e.getKeyCode()) { 
        ... 
0

Personnellement, je suggère d'utiliser une fenêtre ou une JWindow personnalisée au lieu de JPopupMenu comme ce dernier initialement destiné uniquement à afficher uniquement les éléments de menu. Cela fonctionne en général pour d'autres choses, mais ce n'est pas la meilleure pratique pour l'utiliser différemment. Par exemple, vous avez quelques éléments de menu dans votre exemple comme les options de saisie semi-automatique - cela fonctionne très bien s'il n'y a que quelques résultats. Mais que se passe-t-il s'il y en aura 10? Et si 50? Ou 500? Vous devrez créer des solutions de contournement supplémentaires pour ces cas en quelque sorte - soit mettre des éléments dans le volet de défilement (oh mon dieu, cela aurait l'air laid) ou réduire les résultats à quelques-uns (ce qui n'est pas la meilleure option non plus).

J'ai donc fait un petit exemple en utilisant JWindow comme un popup pour le AutocompleteField. Il est assez simple, mais fait quelques choses de base que vous attendez d'elle et aussi ceux que vous avez mentionnés:

import javax.swing.*; 
import javax.swing.border.EmptyBorder; 
import javax.swing.event.DocumentEvent; 
import javax.swing.event.DocumentListener; 
import java.awt.*; 
import java.awt.event.FocusEvent; 
import java.awt.event.FocusListener; 
import java.awt.event.KeyEvent; 
import java.awt.event.KeyListener; 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 
import java.util.function.Function; 
import java.util.stream.Collectors; 

/** 
* @author Mikle Garin 
* @see https://stackoverflow.com/questions/45439231/implementing-autocomplete-with-jtextfield-and-jpopupmenu 
*/ 

public final class AutocompleteField extends JTextField implements FocusListener, DocumentListener, KeyListener 
{ 
    /** 
    * {@link Function} for text lookup. 
    * It simply returns {@link List} of {@link String} for the text we are looking results for. 
    */ 
    private final Function<String, List<String>> lookup; 

    /** 
    * {@link List} of lookup results. 
    * It is cached to optimize performance for more complex lookups. 
    */ 
    private final List<String> results; 

    /** 
    * {@link JWindow} used to display offered options. 
    */ 
    private final JWindow popup; 

    /** 
    * Lookup results {@link JList}. 
    */ 
    private final JList list; 

    /** 
    * {@link #list} model. 
    */ 
    private final ListModel model; 

    /** 
    * Constructs {@link AutocompleteField}. 
    * 
    * @param lookup {@link Function} for text lookup 
    */ 
    public AutocompleteField (final Function<String, List<String>> lookup) 
    { 
     super(); 
     this.lookup = lookup; 
     this.results = new ArrayList<>(); 

     final Window parent = SwingUtilities.getWindowAncestor (this); 
     popup = new JWindow (parent); 
     popup.setType (Window.Type.POPUP); 
     popup.setFocusableWindowState (false); 
     popup.setAlwaysOnTop (true); 

     model = new ListModel(); 
     list = new JList (model); 

     popup.add (new JScrollPane (list) 
     { 
      @Override 
      public Dimension getPreferredSize() 
      { 
       final Dimension ps = super.getPreferredSize(); 
       ps.width = AutocompleteField.this.getWidth(); 
       return ps; 
      } 
     }); 

     addFocusListener (this); 
     getDocument().addDocumentListener (this); 
     addKeyListener (this); 
    } 

    /** 
    * Displays autocomplete popup at the correct location. 
    */ 
    private void showAutocompletePopup() 
    { 
     final Point los = AutocompleteField.this.getLocationOnScreen(); 
     popup.setLocation (los.x, los.y + getHeight()); 
     popup.setVisible (true); 
    } 

    /** 
    * Closes autocomplete popup. 
    */ 
    private void hideAutocompletePopup() 
    { 
     popup.setVisible (false); 
    } 

    @Override 
    public void focusGained (final FocusEvent e) 
    { 
     SwingUtilities.invokeLater (() -> { 
      if (results.size() > 0) 
      { 
       showAutocompletePopup(); 
      } 
     }); 
    } 

    private void documentChanged() 
    { 
     SwingUtilities.invokeLater (() -> { 
      // Updating results list 
      results.clear(); 
      results.addAll (lookup.apply (getText())); 

      // Updating list view 
      model.updateView(); 
      list.setVisibleRowCount (Math.min (results.size(), 10)); 

      // Selecting first result 
      if (results.size() > 0) 
      { 
       list.setSelectedIndex (0); 
      } 

      // Ensure autocomplete popup has correct size 
      popup.pack(); 

      // Display or hide popup depending on the results 
      if (results.size() > 0) 
      { 
       showAutocompletePopup(); 
      } 
      else 
      { 
       hideAutocompletePopup(); 
      } 
     }); 
    } 

    @Override 
    public void focusLost (final FocusEvent e) 
    { 
     SwingUtilities.invokeLater (this::hideAutocompletePopup); 
    } 

    @Override 
    public void keyPressed (final KeyEvent e) 
    { 
     if (e.getKeyCode() == KeyEvent.VK_UP) 
     { 
      final int index = list.getSelectedIndex(); 
      if (index != -1 && index > 0) 
      { 
       list.setSelectedIndex (index - 1); 
      } 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_DOWN) 
     { 
      final int index = list.getSelectedIndex(); 
      if (index != -1 && list.getModel().getSize() > index + 1) 
      { 
       list.setSelectedIndex (index + 1); 
      } 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_ENTER) 
     { 
      final String text = (String) list.getSelectedValue(); 
      setText (text); 
      setCaretPosition (text.length()); 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) 
     { 
      hideAutocompletePopup(); 
     } 
    } 

    @Override 
    public void insertUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void removeUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void changedUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void keyTyped (final KeyEvent e) 
    { 
     // Do nothing 
    } 

    @Override 
    public void keyReleased (final KeyEvent e) 
    { 
     // Do nothing 
    } 

    /** 
    * Custom list model providing data and bridging view update call. 
    */ 
    private class ListModel extends AbstractListModel 
    { 
     @Override 
     public int getSize() 
     { 
      return results.size(); 
     } 

     @Override 
     public Object getElementAt (final int index) 
     { 
      return results.get (index); 
     } 

     /** 
     * Properly updates list view. 
     */ 
     public void updateView() 
     { 
      super.fireContentsChanged (AutocompleteField.this, 0, getSize()); 
     } 
    } 

    /** 
    * Sample {@link AutocompleteField} usage. 
    * 
    * @param args run arguments 
    */ 
    public static void main (final String[] args) 
    { 
     final JFrame frame = new JFrame ("Sample autocomplete field"); 

     // Sample data list 
     final List<String> values = Arrays.asList ("Frame", "Dialog", "Label", "Tree", "Table", "List", "Field"); 

     // Simple lookup based on our data list 
     final Function<String, List<String>> lookup = text -> values.stream() 
       .filter (v -> !text.isEmpty() && v.toLowerCase().contains (text.toLowerCase()) && !v.equals (text)) 
       .collect (Collectors.toList()); 

     // Autocomplete field itself 
     final AutocompleteField field = new AutocompleteField (lookup); 
     field.setColumns (15); 

     final JPanel border = new JPanel (new BorderLayout()); 
     border.setBorder (new EmptyBorder (50, 50, 50, 50)); 
     border.add (field); 
     frame.add (border); 

     frame.setDefaultCloseOperation (WindowConstants.EXIT_ON_CLOSE); 
     frame.pack(); 
     frame.setLocationRelativeTo (null); 
     frame.setVisible (true); 
    } 
} 

Ainsi, dans cet exemple pop-up JWindow lui-même est pas actif (non ciblée) et ne peut pas obtenir le focus comme configuré avec force pour l'être. Cela nous permet de rester concentré au sein de JTextField et de continuer à taper.

Dans cet exemple, nous capturons également des événements clés tels que les flèches HAUT/BAS dans le champ pour naviguer dans les résultats de saisie semi-automatique. Et ENTER et ESCAPE sont utilisés pour accepter/annuler le choix des résultats.

Ce code pourrait aussi être légèrement réécrite pour utiliser swing PopupFactory comme source de la fenêtre de saisie semi-automatique, mais il serait toujours le même dans le Essense depuis HeavyWeightWindow utilisé par PopupFactory étend simplement JWindow et ajoute quelques paramètres.