Отображение / скрытие JPopupMenu из JButton;FocusListener не работает?

StackOverflow https://stackoverflow.com/questions/2421914

Вопрос

Мне нужна была JButton с прикрепленным выпадающим меню в стиле.Итак, я взял JPopupMenu и прикрепил его к JButton так, как вы можете видеть в приведенном ниже коде.Что ему нужно сделать, так это это:

  • показывать всплывающее окно при нажатии
  • скройте его, если нажмете второй раз
  • скройте его, если элемент выбран во всплывающем окне
  • скройте это, если пользователь нажмет где-то в другом месте экрана

Эти 4 вещи работают, но из-за используемого мной флага boolean, если пользователь нажимает где-то еще или выбирает элемент, мне приходится дважды нажать на кнопку, прежде чем она снова появится.Вот почему я попытался добавить FocusListener (который абсолютно не отвечает), чтобы исправить это и установить флаг false в этих случаях.

Редактировать:Последняя попытка в ответном сообщении...

Вот слушатели:(Он находится в классе, расширяющем JButton, поэтому второй прослушиватель находится на JButton.)

// Show popup on left click.
menu.addFocusListener(new FocusListener() {
 @Override
 public void focusLost(FocusEvent e) {
  System.out.println("LOST FOCUS");
  isShowingPopup = false;
 }

 @Override
 public void focusGained(FocusEvent e) {
  System.out.println("GAINED FOCUS");
 }
});

addActionListener(new ActionListener() {
 @Override
 public void actionPerformed(ActionEvent e) {
  System.out.println("isShowingPopup: " + isShowingPopup);
  if (isShowingPopup) {
   isShowingPopup = false;
  } else {
   Component c = (Component) e.getSource();
   menu.show(c, -1, c.getHeight());
   isShowingPopup = true;
  }
 }
});

Я боролся с этим слишком долго.Если кто-нибудь сможет дать мне подсказку о том, что в этом плохого, это было бы здорово!

Спасибо!

Код:

public class Button extends JButton {

    // Icon.
    private static final ImageIcon ARROW_SOUTH = new ImageIcon("ArrowSouth.png");

    // Unit popup menu.
    private final JPopupMenu menu;

    // Is the popup showing or not?
    private boolean isShowingPopup = false;

    public Button(int height) {
        super(ARROW_SOUTH);
        menu = new JPopupMenu(); // menu is populated somewhere else

        // FocusListener on the JPopupMenu
        menu.addFocusListener(new FocusListener() {
            @Override
            public void focusLost(FocusEvent e) {
                System.out.println("LOST FOCUS");
                isShowingPopup = false;
            }

            @Override
            public void focusGained(FocusEvent e) {
                System.out.println("GAINED FOCUS");
            }
        });

        // ComponentListener on the JPopupMenu
        menu.addComponentListener(new ComponentListener() {
            @Override
            public void componentShown(ComponentEvent e) {
                System.out.println("SHOWN");
            }

            @Override
            public void componentResized(ComponentEvent e) {
                System.out.println("RESIZED");
            }

            @Override
            public void componentMoved(ComponentEvent e) {
                System.out.println("MOVED");
            }

            @Override
            public void componentHidden(ComponentEvent e) {
                System.out.println("HIDDEN");
            }
        });

        // ActionListener on the JButton
        addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("isShowingPopup: " + isShowingPopup);
                if (isShowingPopup) {
                    menu.requestFocus();
                    isShowingPopup = false;
                } else {
                    Component c = (Component) e.getSource();
                    menu.show(c, -1, c.getHeight());
                    isShowingPopup = true;
                }
            }
        });

        // Skip when navigating with TAB.
        setFocusable(true); // Was false first and should be false in the end.

        menu.setFocusable(true);
    }

}
Это было полезно?

Решение

Вот еще один подход, который не так уж плох для взлома, если не элегантен, и который, насколько я мог судить, работает.Сначала, в самом верху, я добавил второе логическое значение, называемое showPopup.

В FocusListener должно быть следующим:

    menu.addFocusListener(new FocusListener() {
        @Override
        public void focusLost(FocusEvent e) {
            System.out.println("LOST FOCUS");
            isShowingPopup = false;
        }

        @Override
        public void focusGained(FocusEvent e) {
            System.out.println("GAINED FOCUS");
            isShowingPopup = true;
        }
    });

В isShowingPopup логическое значение больше нигде не изменяется - если оно получает фокус, предполагается, что оно показано, а если оно теряет фокус, предполагается, что это не так.

Далее, в ActionListener на кнопке по-другому:

   addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("isShowingPopup: " + isShowingPopup);
            if (showPopup) {
                Component c = (Component) e.getSource();
                menu.show(c, -1, c.getHeight());
                menu.requestFocus();
            } else {
                showPopup = true;
            }
        }
    });

Теперь начинается по-настоящему новое.Это MouseListener на кнопке:

    addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
            System.out.println("ispopup?: " + isShowingPopup);
            if (isShowingPopup) {
                showPopup = false;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            showPopup = true;
        }
    });

В основном, mousePressed вызывается до того, как меню потеряет фокус, так что isShowingPopup отражает, было ли показано всплывающее окно до нажатия кнопки.Затем, если меню было там, мы просто устанавливали showPopup Для false, так что actionPerformed метод не показывает меню после его вызова (после того, как мышь отпущена).

Это вело себя так, как ожидалось, во всех случаях, кроме одного:если меню отображалось, и пользователь нажал мышью на кнопку, но отпустил ее за ее пределами, actionPerformed его никогда не вызывали.Это означало , что showPopup оставалось ложным, и меню не отображалось при следующем нажатии кнопки.Чтобы исправить это, mouseReleased сброс метода showPopupmouseReleased метод вызывается после actionPerformed, насколько я могу судить.

Я немного поиграл с результирующей кнопкой, проделывая с ней все, что мог придумать, и она сработала так, как ожидалось.Однако я не уверен на 100%, что события всегда будут происходить в одном и том же порядке.

В конечном счете, я думаю, что это, по крайней мере, стоит попробовать.

Другие советы

Вот вариант предложения Эмбер Шах "большой взлом", которое я только что сделал.Без флага isShowingPopup...

Это не пуленепробиваемый, но он работает довольно хорошо, пока кто-то не придет с невероятно медленным щелчком, чтобы закрыть всплывающее окно (или очень быстрым вторым щелчком, чтобы снова открыть его ...).

public class Button extends JButton {

 // Icon.
 private static final ImageIcon ARROW_SOUTH = new ImageIcon("ArrowSouth.png");

 // Popup menu.
 private final JPopupMenu menu;

 // Last time the popup closed.
 private long timeLastShown = 0;

 public Button(int height) {
  super(ARROW_SOUTH);
  menu = new JPopupMenu(); // Populated somewhere else.

  // Show and hide popup on left click.
  menu.addPopupMenuListener(new PopupMenuListener() {
   @Override
   public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) {
    timeLastShown = System.currentTimeMillis();
   }
   @Override public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {}
   @Override public void popupMenuCanceled(PopupMenuEvent arg0) {}
  });
  addActionListener(new ActionListener() {
   @Override
   public void actionPerformed(ActionEvent e) {
    if ((System.currentTimeMillis() - timeLastShown) > 300) {
     Component c = (Component) e.getSource();
     menu.show(c, -1, c.getHeight());
    }
   }
  });

  // Skip when navigating with TAB.
  setFocusable(false);
 }

}

Как я уже говорил в комментариях, это не самое элегантное решение, но оно ужасно простое и работает в 98% случаев.

Открыт для предложений!

Вы могли бы использовать JPopupMenu.Является видимым() вместо вашей логической переменной для проверки текущего состояния всплывающего меню.

Вы пробовали добавить ComponentListener к тому JPopupMenu, чтобы вы знали , когда это было показано и скрыто (и обновляли свой isShowingPopup флаг соответственно)?Я не уверен, что прислушиваться к изменениям фокуса - обязательно правильный подход.

Что вам нужно, так это PopupMenuListener:

        menu.addPopupMenuListener(new PopupMenuListener() {

            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {

            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) {
                System.out.println("MENU INVIS"); 
                isShowingPopup = false;     
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent arg0) {
                System.out.println("MENU CANCELLED"); 
                isShowingPopup = false;                     
            }
        });

Я вставил это в ваш код и убедился, что это работает.

Ну, я не могу быть уверен, не видя всего вашего кода, но возможно ли, что всплывающее окно вообще никогда не получает фокуса?У меня и раньше были проблемы с тем, что вещи не фокусировались должным образом в Swing, так что это могло быть причиной.Попробуйте позвонить setFocusable(true) в меню, а затем вызывая requestFocus() когда вы заставите появиться меню.

Я попробовал ответ Тихона Джелвиса (представляя разумную комбинацию FocusListener и MouseListener).У меня это не работает в Linux (Java7 / gtk).:-(

Чтение http://docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#requestFocus%28%29 там написано "Обратите внимание, что использование этого метода не рекомендуется, поскольку его поведение зависит от платформы".

Возможно, порядок вызовов прослушивателя изменился с помощью Java7 или он меняется с помощью GTK vs Windows.Я бы не рекомендовал это решение, если вы хотите быть независимым от платформы.

КСТАТИ:Я создал новую учетную запись в stackoverflow, чтобы дать эту подсказку.Похоже, мне не разрешено комментировать его ответ (из-за репутации).Но, кажется, у меня есть кнопка для его редактирования.Этот stackoverflow - очень забавная штука.:-)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top