Отображение / скрытие JPopupMenu из JButton;FocusListener не работает?
-
19-09-2019 - |
Вопрос
Мне нужна была 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
сброс метода showPopup
.В mouseReleased
метод вызывается после 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 - очень забавная штука.:-)