Mostrando / escondendo uma JPopupMenu de um Jbutton; FocusListener não está funcionando?
-
19-09-2019 - |
Pergunta
Eu precisava de um JButton com um menu anexado estilo suspensa. Então eu peguei uma JPopupMenu e anexado-lo para o JButton na forma como você pode ver no código abaixo. O que ele precisa fazer é esta:
- mostrar o pop-up quando clicado
- escondê-lo se clicado uma segunda vez
- escondê-lo, se um item é selecionado no pop-up
- escondê-lo se o usuário clicar em algum outro lugar da tela
Estes 4 coisas trabalho, mas por causa do sinalizador booleano que estou usando, se o usuário clicar em algum outro lugar ou seleciona um item, eu tenho que clicar duas vezes no botão antes de ele mostra-se novamente. É por isso que eu tentei adicionar um focusListener (que absolutamente não está respondendo) a correção e definir a bandeira falsa nestes casos.
EDIT: Última tentativa em um post resposta ...
Aqui estão os ouvintes: (é em uma classe estendendo JButton, então o segundo ouvinte está no 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;
}
}
});
Eu estive lutando com isso por muito tempo agora. Se alguém pode me dar uma pista sobre o que há de errado com isso, seria ótimo!
Obrigado!
Código:
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);
}
}
Solução
Aqui é uma outra abordagem que não é tão ruim de um truque, se não elegante, e que, tanto quanto eu poderia dizer, obras. Em primeiro lugar, no topo, eu adicionei um segundo boolean chamado showPopup
.
O FocusListener
tem de ser a seguinte:
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;
}
});
O boolean isShowingPopup
não ter mudado em qualquer outro lugar - se ele ganha o foco, ele assume que é mostrado e se perde o foco, ele assume que não é
Em seguida, o ActionListener
no botão é diferente:
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;
}
}
});
Agora vem o realmente novo bit. É um MouseListener
no botão:
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;
}
});
Basicamente, mousePressed
é chamado antes que o menu perde o foco, assim isShowingPopup
reflete se o popup foi mostrado antes que o botão é pressionado. Então, se o menu foi lá, nós apenas definir showPopup
para false
, para que o método actionPerformed
não mostra o menu, uma vez que é chamado (após o mouse é deixar de ir).
Esta comportou como esperado em cada caso, mas um: se o menu foi mostrando e o usuário pressionou o mouse sobre o botão, mas lançou fora dele, actionPerformed
nunca foi chamado. Isto significava que showPopup
permaneceu falso e o menu não foi mostrado na próxima vez que o botão foi pressionado. Para corrigir isso, as redefinições de métodos mouseReleased
showPopup
. O método mouseReleased
é chamado após actionPerformed
, tanto quanto eu posso dizer.
Eu brinquei com o botão resultando um pouco, fazendo todas as coisas que eu poderia pensar para o botão, e funcionou como esperado. No entanto, eu não estou 100% de certeza que os eventos irão acontecer sempre na mesma ordem.
Em última análise, acho que esta é, pelo menos, vale a pena tentar.
Outras dicas
Aqui está uma variante da sugestão "grande corte" de Amber Shah Eu só fiz. Sem o sinalizador isShowingPopup ...
Não é à prova de balas, mas funciona muito bem até que alguém chega com um clique incrivelmente lento para fechar o pop-up (ou um muito rápido segunda clique para reabri-lo ...).
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);
}
}
Como eu disse nos comentários, isso não é a solução mais elegante, mas é terrivelmente simples e funciona em 98% dos casos.
Abrir para sugestões!
Você pode usar o JPopupMenu.isVisible () em vez de seu variável booleana para verificar o estado actual do menu pop-up.
Você tentou adicionar um ComponentListener
ao JPopupMenu
, para que você saiba quando ele foi mostrado e escondido (e atualizar sua bandeira isShowingPopup
conformidade)? Eu não tenho certeza de escuta para alterações de foco é necessariamente a abordagem correta.
O que você precisa é um 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;
}
});
I inserido isso em seu código e verificou-se que ele funciona.
Bem, eu não posso ter certeza sem ver todo o seu código, mas é possível que o pop-up nunca realmente recebe foco em tudo? Eu tive problemas com as coisas não recebendo foco corretamente no balanço antes, assim que poderia ser o culpado. Tente chamar setFocusable(true)
no menu e, em seguida, chamando requestFocus()
quando você fazer o menu aparecer.
Eu tentei a resposta de Tíkhon Jelvis (introdução de uma combinação inteligente de focusListener e MouseListener). Ele não funciona para mim no Linux (Java7 / GTK). : - (
http: / /docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#requestFocus%28%29 lá está escrito "Note-se que o uso deste método não é recomendado porque o seu comportamento é plataforma dependente. "
Pode ser que a ordem das chamadas ouvinte mudou com Java7 ou ele muda com o GTK vs do Windows. Eu não recomendaria essa solução se você quer ser independente de plataforma.
BTW: Eu criei uma nova conta no stackoverflow para dar essa dica. Parece que eu não estou autorizado a comentar a sua resposta (por causa da reputação). Mas parece que eu tenho um botão para editá-lo. Este stackoverflow é uma coisa muito engraçada. : -)