We have recently attached a GWT MenuBar to a part of our application for, well, menu purposes.
Basically I want the sub menus to open when you mouse over the top level menu, which is easy enough to do:
menubar.setAutoOpen(true);
I would also like to have the sub menu automatically hide when the user's mouse leaves the sub menu. Ideally with some sort of delay to prevent it vanishing too abruptly, but I'd settle for just the hiding.
This doesn't seem to be built in and the MenuItem object in GWT directly subclasses UIObject which means there isn't a relatively trivial onBrowserEvent() or somewhere to attach mouse listeners. Possibly extending MenuItem and sinking/unsinking events would let me add this behavior, but I am unsure if that is the best approach.
So what would be the best approach to autohiding a GWT submenu?
Thank you.
After much horrible hacking trying to achieve something similar we wrote our own cascading menu as part of the GWT Portlets framework. It displays menu items and submenus from an HTML template looking something like this:
Home
Sub Menu 1
Away
<div id="submenu1">
Hello World
Free Memory
Sub Menu 2
</div>
<div id="submenu2">
Command Demo
Command1
Command2
</div>
The URLs that look like method calls broadcast CommandEvent's. The others trigger a history token change like normal. Have a look at the online demo to see the menu in action.
Here's a fairly complete solution, not perfect, explained after code:
public class MyMenuBar extends Composite {
private class OpenTab implements ScheduledCommand {
private String wid;
public OpenTab(String windowId) {
wid = windowId;
}
#Override
public void execute() {
WinUtl.newAppTab(wid);
}
}
interface MyMenuBarUiBinder extends UiBinder<Widget, MyMenuBar> {}
private static MyMenuBarUiBinder uiBinder =
GWT.create(MyMenuBarUiBinder.class);
#UiField MenuBar mainMenu;
#UiField MenuBar subsMenu;
#UiField MenuItem subsChoice1;
#UiField MenuItem subsChoice2;
#UiField MenuItem subsChoice3;
#UiField MenuBar svcPrvdrMenu;
#UiField MenuItem svcPrvdrChoice1;
#UiField MenuItem svcPrvdrChoice2;
#UiField MenuBar netMgtMenu;
#UiField MenuItem netMgtChoice1;
#UiField MenuBar reportsMenu;
#UiField MenuItem reportsChoice1;
#UiField MenuBar auditsMenu;
#UiField MenuItem auditsChoice1;
#UiField MenuBar securityMenu;
#UiField MenuItem securityChoice1;
#UiField MenuBar helpMenu;
#UiField MenuItem helpChoice1;
private boolean subMenuPopped = false;
private boolean subMenuEntered = false;
private static Type<MouseOverHandler> OVR_EVT = MouseOverEvent.getType();
private static Type<MouseOutHandler> OUT_EVT = MouseOutEvent.getType();
private MouseOverHandler mainOverHandler = new MouseOverHandler() {
#Override
public void onMouseOver(MouseOverEvent event) {
subMenuPopped = true;
}
};
private MouseOutHandler mainOutHandler = new MouseOutHandler() {
#Override
public void onMouseOut(MouseOutEvent event) {
Element e = event.getRelativeElement()
boolean movedUp = (event.getRelativeY(e) < 0);
if ((movedUp && subMenuPopped) || subMenuEntered) {
subMenuPopped = false;
subMenuEntered = false;
mainMenu.closeAllChildren(true);
}
}
};
private MouseOverHandler subOverHandler = new MouseOverHandler() {
#Override
public void onMouseOver(MouseOverEvent event) {
subMenuEntered = true;
}
};
private MouseOutHandler subOutHandler = new MouseOutHandler() {
#Override
public void onMouseOut(MouseOutEvent event) {
subMenuPopped = false;
subMenuEntered = false;
mainMenu.closeAllChildren(true);
}
};
public MyMenuBar() {
initWidget(uiBinder.createAndBindUi(this));
mainMenu.addStyleName("npac-MenuBar");
mainMenu.setAutoOpen(true);
mainMenu.setAnimationEnabled(true);
mainMenu.setFocusOnHoverEnabled(true);
subsChoice1.setScheduledCommand(new OpenTab(Names.Wid.NPA));
mainMenu.addDomHandler(mainOverHandler, OVR_EVT);
mainMenu.addDomHandler(mainOutHandler, OUT_EVT);
addHandlers(subsMenu);
addHandlers(svcPrvdrMenu);
addHandlers(netMgtMenu);
addHandlers(reportsMenu);
addHandlers(auditsMenu);
addHandlers(securityMenu);
addHandlers(helpMenu);
}
private void addHandlers(MenuBar m) {
m.addDomHandler(subOverHandler, OVR_EVT);
m.addDomHandler(subOutHandler, OUT_EVT);
}
}
This handles the case where mouseOver opens subMenu, user then mouses UP, off mainMenu (subMenu closes). It does not handle the mouse moving diagonally down, past either side of subMenu (submenu stays open)
Certainly can be improved, but I just got it to work and wanted to share ;-)
There's no need for horrible hacking or relying on CSS within JAVA to achieve autohiding of a MenuBar with submenus. I created a fully working example of a Parent+Children dropdown menu with mouseover opening and mouseOut closing with explanations of each part for others to use.
The common problem I've witnessed folks having is Running a ((JMenu)e.getSource()).doClick(); on the mouseEntered simulates the click into one of the JMenu parents but can't be simply added to the mouseExited method as the MouseListener needs to be attached to the child MenuItems as well as the JMenu parents. (Which it doesn't do in the normal assignment to the MenuBar - only attaching to the parent JMenu objects).
Additionally, a problem arises due to trying to get the MouseExit listener to fire a "close" method ONLY when the mouse has left the entire Menu structure (ie the Child menu dropdowns).
Below is a fully working answer taken from my live app:
The way I solved the menu close on mouse out was to run a boolean variable "isMouseOut" in the top of the constructor to keep track, and then allocate the MouseListener in a more OO friendly way to keep track of the multiple MouseIn-MouseOut events as a user interacts with the menu. Which calls a separate menuClear method acting upon the state of the boolean "isMouseOut". The class implements MouseListener. This is how its done.
Create an ArrayList adding all the menu items to this array first. Like so:
Font menuFont = new Font("Arial", Font.PLAIN, 12);
JMenuBar menuBar = new JMenuBar();
getContentPane().add(menuBar, BorderLayout.NORTH);
// Array of MenuItems
ArrayList<JMenuItem> aMenuItms = new ArrayList<JMenuItem>();
JMenuItem mntmRefresh = new JMenuItem("Refresh");
JMenuItem mntmNew = new JMenuItem("New");
JMenuItem mntmNormal = new JMenuItem("Normal");
JMenuItem mntmMax = new JMenuItem("Max");
JMenuItem mntmStatus = new JMenuItem("Status");
JMenuItem mntmFeedback = new JMenuItem("Send Feedback");
JMenuItem mntmEtsyTWebsite = new JMenuItem("EtsyT website");
JMenuItem mntmAbout = new JMenuItem("About");
aMenuItms.add(mntmRefresh);
aMenuItms.add(mntmNew);
aMenuItms.add(mntmNormal);
aMenuItms.add(mntmMax);
aMenuItms.add(mntmStatus);
aMenuItms.add(mntmFeedback);
aMenuItms.add(mntmEtsyTWebsite);
aMenuItms.add(mntmAbout);
then iterate over the arrayList at this stage adding a MouseListener using the for() loop:
for (Component c : aMenuItms) {
if (c instanceof JMenuItem) {
c.addMouseListener(ml);
}
}
Now set JMenu parents for the MenuBar:
// Now set JMenu parents on MenuBar
final JMenu mnFile = new JMenu("File");
menuBar.add(mnFile).setFont(menuFont);
final JMenu mnView = new JMenu("View");
menuBar.add(mnView).setFont(menuFont);
final JMenu mnHelp = new JMenu("Help");
menuBar.add(mnHelp).setFont(menuFont);
Then add the dropdown menuItems children to the JMenu parents:
// Now set menuItems as children of JMenu parents
mnFile.add(mntmRefresh).setFont(menuFont);
mnFile.add(mntmNew).setFont(menuFont);
mnView.add(mntmNormal).setFont(menuFont);
mnView.add(mntmMax).setFont(menuFont);
mnHelp.add(mntmStatus).setFont(menuFont);
mnHelp.add(mntmFeedback).setFont(menuFont);
mnHelp.add(mntmEtsyTWebsite).setFont(menuFont);
mnHelp.add(mntmAbout).setFont(menuFont);
Add the mouseListeners to the JMenu parents as a separate step:
for (Component c : menuBar.getComponents()) {
if (c instanceof JMenu) {
c.addMouseListener(ml);
}
}
Now that the child menuItem elements all have their own listeners that are separate to the parent JMenu elements and the MenuBar itself - It is important to identify the object type within the MouseListener() instantiation so that you get the menu auto opening on mouseover (in this example the 3x JMenu parents) BUT ALSO avoids child exception errors and allows clean identification of mouseOUT of the menu structure without trying to monitor where the mouse position is. The MouseListener is as follows:
MouseListener ml = new MouseListener() {
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
isMouseOut = true;
timerMenuClear();
}
public void mouseEntered(MouseEvent e) {
isMouseOut = false;
Object eSource = e.getSource();
if(eSource == mnHelp || eSource == mnView || eSource == mnFile){
((JMenu) eSource).doClick();
}
}
};
The above only simulates the mouse click into the JMenu 'parents' (3x in this example) as they are the triggers for the child menu dropdowns. The timerMenuClear() method calls on the MenuSelectionManager to empty whatever selectedpath point was live at the time of real mouseOUT:
public void timerMenuClear(){
ActionListener task = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(isMouseOut == true){
System.out.println("Timer");
MenuSelectionManager.defaultManager().clearSelectedPath();
}
}
};
//Delay timer half a second to ensure real mouseOUT
Timer timer = new Timer(1000, task);
timer.setInitialDelay(500);
timer.setRepeats(false);
timer.start();
}
It took me a little testing, monitoring what values I could access within the JVM during its development - but it Works a treat! even with nested menus :) I hope many find this full example very useful.
Use this code:
public class MenuBarExt extends MenuBar {
public MenuBarExt()
{
super();
}
#Override
public void onBrowserEvent(Event event)
{
switch (DOM.eventGetType(event))
{
case Event.ONMOUSEOUT:
closeAllChildren(false);
break;
default:
super.onBrowserEvent(event);
break;
}
super.onBrowserEvent(event);
}
}
Related
So I have a JFrame set up with a menu with the current structure that looks something along the lines of this:
File
Exit
Pages
Reviews
A
B
C
Help
About
I want to create a Action Listener that only listens to menu items under Reviews. Is this a possibility (and if so, how) or do I have to create a generic listener and check if it's one of those items?
Yes, it is possible:
Store your menu items as fields
Add the same ActionListener to each menu item.
In the listener check for the source to know which item was clicked.
Should look like:
public class YourFrame extends JFrame implements ActionListener {
private final JMenuItem menuA, menuB;
public YourFrame(){
super("Your app");
JMenuBar menuBar = new JMenuBar();
JMenu menuReviews = new JMenu("Reviews");
menuA = new JMenuItem("A");
menuB = new JMenuItem("B");
...
menuReviews.add(menuA);
menuReviews.add(menuB);
menuBar.add(menuReviews);
setJMenuBar(menuBar);
...
menuA.addActionListener(this);
menuB.addActionListener(this);
...
}
public void actionPerformed(ActionEvent event){
if(event.getSource()==menuA){
System.out.println("Menu A clicked");
...
}else if(event.getSource()==menuB){
System.out.println("Menu B clicked");
...
}
}
}
Note that here I let the JFrame implement ActionListener, but this is just for convenience. You could use a dedicated class, or an anonymous class created in the constructor:
ActionListener reviewsListener = new ActionListener(){
public void actionPerformed(ActionEvent event){
if(event.getSource()==menuA){
System.out.println("Menu A clicked");
...
}else if(event.getSource()==menuB){
System.out.println("Menu B clicked");
...
}
}
};
menuA.addActionListener(reviewsListener);
menuB.addActionListener(reviewsListener);
If you want to integrate this process a little more, I could also suggest to extend JMenu, so that you can pass it your action listener and add it systematically to new menu items.
public class YourJMenu extends JMenu {
private ActionListener listener;
public YourJMenu(String name, ActionListener listener){
super(name);
this.listener = listener;
}
#Override
public JMenuItem add(JMenuItem item){
item.addActionListener(listener);
return super.add(item);
}
}
With this, you just need to write:
JMenu menuReviews = new YourJMenu("Reviews", this);
and drop the:
menuA.addActionListener(this);
menuB.addActionListener(this);
Using a common method we can add the action listener to all the menu items under a menu. Below is a example code.
public class MenuItemEvent {
JFrame objFrm = new JFrame("Menu event demo");
JMenuBar mBar;
JMenu mnu;
JMenuItem mnuItem1, mnuItem2, mnuItem3;
public void show() {
objFrm.setSize(300, 300);
mBar = new JMenuBar();
mnu = new JMenu("Reviews");
mBar.add(mnu);
mnuItem1 = new JMenuItem("A");
mnu.add(mnuItem1);
mnuItem2 = new JMenuItem("B");
mnu.add(mnuItem2);
mnuItem3 = new JMenuItem("C");
mnu.add(mnuItem3);
//method call
fnAddActionListener(mnu);
objFrm.setJMenuBar(mBar);
objFrm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
objFrm.setVisible(true);
}
//method to add action listener to all menu items under a menu
public void fnAddActionListener(JMenu mnu) {
if (mnu.getItemCount() != 0) {
for (int iCount = 0; iCount < mnu.getItemCount(); iCount++) {
(mnu.getItem(iCount)).addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fnMenuItemAction(e);
}
});
}
}
}
//menu item action event
public void fnMenuItemAction(ActionEvent e) {
if (e.getSource().equals(mnuItem1)) {
System.out.println("Menu Item 1");
} else if (e.getSource().equals(mnuItem2)) {
System.out.println("Menu Item 2");
} else if (e.getSource().equals(mnuItem3)) {
System.out.println("Menu Item 3");
}
}
public static void main(String[] args) {
new MenuItemEvent().show();
}
}
or with the below function
//fnMenuItemAdd(mnu,mnuItem1)
//etc.
public void fnMenuItemAdd(JMenu mnu, JMenuItem mni) {
mnu.add(mni);
mni.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fnMenuItemAction(e);
}
});
}
I'm trying to create a JPopupMenu, but for some reason, it doesn't show the text I've set on the JMenuItems. The menu itself works, there are menuitems in it and they are responsive, but the text is not showing. I'm creating the menu like this:
private void createPopupMenu() {
this.popupMenu = new JPopupMenu();
this.addMouseListener(new PopupListener(this));
JMenuItem addPlaceMenuItem = new JMenuItem(SketchPad.ADD_PLACE_POPUP_TEXT);
addPlaceMenuItem.setAction(new PopupAction(ActionType.AddPlace));
this.popupMenu.add(addPlaceMenuItem);
JMenuItem addTransitionMenuItem = new JMenuItem(SketchPad.ADD_TRANSITION_POPUP_TEXT);
addTransitionMenuItem.setAction(new PopupAction(ActionType.AddTransition));
this.popupMenu.add(addTransitionMenuItem);
}
In case it matters, here is the PopupListener:
class PopupListener extends MouseAdapter {
SketchPad pad;
public PopupListener(SketchPad pad)
{
this.pad = pad;
}
public void mousePressed(MouseEvent e) {
maybeShowPopup(e);
}
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1)
{
this.pad.getController().deselectAllNodes();
}
else
{
maybeShowPopup(e);
}
}
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
pad.popupPosition = new Point(e.getX(), e.getY());
pad.popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
What am I missing here?
but for some reason, it doesn't show the text I've set on the JMenuItems.
addPlaceMenuItem.setAction(new PopupAction(ActionType.AddPlace));
The setAction(...) method reset the properties of the menu item with the properties of the Action. So you need to make sure you set the NAME property of the Action to set the text of the menu item.
So in your case it looks like the value of the NAME property should be:
SketchPad.ADD_PLACE_POPUP_TEXT
Or the other approach is to reset the text of the menu item after you set the Action
JMenuItem addPlaceMenuItem = new JMenuItem( new PopupAction(ActionType.AddPlace) );
addPlaceMenuItem.setText(SketchPad.ADD_PLACE_POPUP_TEXT);
The effect is platform specific. In particular, "In Microsoft Windows, the user by convention brings up a popup menu by releasing the right mouse button while the cursor is over a component that is popup-enabled." Your implementation of mouseReleased() precludes even checking isPopupTrigger(). Instead, handle the selection and check the trigger. A similar approach is shown in GraphPanel in order to handle multiple selection and a context menu.
I'm creating custom popup menu, using just extended JComponent as a menu items and extended JWindow to hold them. My question is - how to send signal from JComponent instance when it's clicked (has MouseListener) to JTextField to perform cut/copy/paste actions?
EDIT:
I will try to explain more precisely.
JTextField class (simplified):
public class TextInputField extends JTextField implements FocusListener {
private MenuPopupWindow popUp;
public TextInputField() {
popUp = new MenuPopupWindow();//MenuPopupWindow class extends JWindow
MenuItem paste = new MenuItem("Paste",
new ImageIcon(getClass().getResource("/images/paste_icon.png")),
"Ctrl+V");//MenuItem class extends JComponent, has implemented MouseListener - and when mouseClicked(MouseEvent e) occurs, somehow action signal have to be sent to this class
MenuItem copy = ....
MenuItem cut = ....
Action pasteAction = getActionMap().get(DefaultEditorKit.pasteAction);
paste.setAction(pasteAction);//How to make it to work?
popUp.addMenuItem(paste);
popUp.addMenuItem(cut);
popUp.addMenuItem(copy);
}
}
How to do it right?
I'm creating custom popup menu, using just extended JComponent as a menu items and extended JWindow to hold them.
Not really sure what all that means.
You should just use a JPopupMenu and add JMenuItems to it. Read the section from the Swing tutorial on Bringing Up a Popup Menu for an example.
Then, if you want cut/copy/paste functionality, you can use the default actions provided by the DefaultEditorKit:
popup.add( new JMenuItem(new DefaultEditorKit.CopyAction()) );
In light of your posted code, I think all you need to do in your TextInputField class, is add:
paste.addActionListener(pasteAction);
then in your MenuItem class you have to put in code to call those action listeners.
public class MenuItem implements MouseListener
{
...
#Override public void mouseClicked(MouseEvent event)
{
ActionListener[] listeners = (ActionListener[])
MenuItem.this.getListeners(ActionListener.class);
for(int i = 0; i < listeners.length; i++)
{
listeners[i].actionPerformed
(
new ActionEvent(MenuItem.this,someID, someCMDName)
);
}
}
In your class that extends JComponent (I'll call it class 'A') you will need to get a reference to your JTextField. A simple way to do this is to add a private instance variable of type JTextField to class A, and pass in the JTextField through the constructor.
so your class should look something like this:
public class A extends JComponent implements ActionListener
{
private JTextField updateField;
public A(JTextField updateField[,<your other contructor arguments>...])
{
this.updateField = updateField;
this.addActionListener(this);
}
public void actionPerformed(ActionEvent event)
{
if(event.getSource().equals(this)
{
//copy, paste or do whatever with the JTextField
//by way of this.updateField;
//e.g. this.updateField.setText(...);
//or to simply pass the event along to the JTextField's handlers
//this.updateField.dispatchEvent(event);
}
}
}
then you just have to remember to pass the jtextField into the constructor when you create your component
So my working example follows (simplified):
public class TextInputField extends JTextField {
private MenuPopupWindow popUp;
private MenuItem copy,
cut,
paste,
selectAll;
public TextInputField() {
popUp = new MenuPopupWindow();
paste = new MenuItem(this, "Paste", new ImageIcon(getClass().getResource("/images/Paste-icon.png")), "Ctrl+V");
cut = new MenuItem(this, "Cut", new ImageIcon(getClass().getResource("/images/Cut-icon.png")), "Ctrl+X");
copy = new MenuItem(this, "Copy", new ImageIcon(getClass().getResource("/images/Copy-icon.png")), "Ctrl+C");
selectAll = new MenuItem(this, "Select All", null, "Ctrl+A");
popUp.addMenuItem(paste);
popUp.addMenuItem(cut);
popUp.addMenuItem(copy);
popUp.addMenuItem(selectAll);
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
if (getSelectedText() == null) {
copy.setEnabled(false);
cut.setEnabled(false);
} else {
copy.setEnabled(true);
cut.setEnabled(true);
}
if (getText().equals("")) {
selectAll.setEnabled(false);
} else {
selectAll.setEnabled(true);
}
Clipboard c = getToolkit().getSystemClipboard();
Transferable t = c.getContents(this);
if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String s;
try {
s = (String) t.getTransferData(DataFlavor.stringFlavor);
if (s.equals("")) {
paste.setEnabled(false);
} else {
paste.setEnabled(true);
}
} catch (UnsupportedFlavorException | IOException ex) {
Logger.getLogger(TextInputField.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
paste.setEnabled(false);
}
popUp.setLocation(e.getXOnScreen(), e.getYOnScreen());
getCaret().setVisible(false);
popUp.setVisible(true);
} else {
Object obj = e.getSource();
if (obj instanceof MenuItem) {
MenuItem menuItem = (MenuItem) obj;
if (paste == menuItem) {
paste();
} else if (cut == menuItem) {
cut();
} else if (copy == menuItem) {
copy();
} else if (selectAll == menuItem) {
selectAll();
}
}
getCaret().setVisible(true);
popUp.setVisible(false);
}
}
});
}
}
And at MenuItem class added (simplified):
#Override
public void mouseClicked(MouseEvent e) {
textField.dispatchEvent(e);
}
Works superb :)
Decided to use Component instead of JComponent in MenuItem class, because there is no need for paintComponent, paintBorder and paintChildren constructors - saving resources.
Does anyone know how to control the display of the tiny little arrow that appears on submenus of JMenu?
Can I change it?
Can I disable it?
Can I move it?
Also, I notice that this arrow doesn't appear on top level JMenus only when they are submenus of other JMenu. This inconsistency annoys me since I have a mix of JMenuItem and JMenu attached to the root of my JMenuBar and so I wish it would always indicate it. Anyway to do this as well?
thanks!
Take a look at the Menu.arrowIcon UI property
(Thanks to AndrewThompson for the test code).
Doining this will effect ALL the menus created AFTER you apply the modifications.
So after you init Look and Feel and before you create any menus call UIManager.getLookAndFeelDefaults().put("Menu.arrowIcon", null);
I'd just like to say I think this is a terrible idea and would highly discourage you from doing it.
this arrow doesn't appear on top level JMenus only when they are submenus of other JMenu.
It seem (monotonously) consistent in its appearance using Metal here.
import javax.swing.*;
public class MenuArrows {
MenuArrows() {
JMenuBar mb = new JMenuBar();
JMenu root1 = new JMenu("Root Menu 1");
JMenu root2 = new JMenu("Root Menu 2");
addSubMenus(root1, 5);
addSubMenus(root2, 3);
mb.add(root1);
mb.add(root2);
JOptionPane.showMessageDialog(null, mb);
}
public void addSubMenus(JMenu parent, int number) {
for (int i=1; i<=number; i++) {
JMenu menu = new JMenu("Sub Menu " + i);
parent.add(menu);
addSubMenus(menu, number-1);
addMenuItems(menu, number);
}
}
public void addMenuItems(JMenu parent, int number) {
for(int i=1; i<=number; i++) {
parent.add(new JMenuItem("Item " + i));
}
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
new MenuArrows();
}
};
SwingUtilities.invokeLater(r);
}
}
I have a JTextField for which I'm hoping to suggest results to match the user's input. I'm displaying these suggestions in a JList contained within a JPopupMenu.
However, when opening the popup menu programmatically via show(Component invoker, int x, int y), the focus is getting taken from the JTextField.
Strangely enough, if I call setVisible(true) instead, the focus is not stolen; but then the JPopupMenu is not attached to any panel, and when minimizing the application whilst the box is open, it stays painted on the window.
I've also tried to reset the focus to the JTextField using requestFocus(), but then I have to restore the caret position using SwingUtilities.invokeLater(), and the invoke later side of things is giving the user a slight margin to mess around with the existing contents / overwrite it / or do other unpredictable things.
The code I've got is effectively:
JTextField field = new JTextField();
JPopupMenu menu = new JPopupMenu();
field.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
JList list = getAListOfResults();
menu.add(list);
menu.show(field, 0, field.getHeight());
}
});
Can anyone suggest the best avenue to go down to show the JPopupMenu programmatically whilst preserving the focus on the JTextField?
The technical answer is to set the popup's focusable property to false:
popup.setFocusable(false);
The implication is that the textField has to take over all keyboard and mouse-triggered actions that are normally handled by the list itself, sosmething like:
final JList list = new JList(Locale.getAvailableLocales());
final JPopupMenu popup = new JPopupMenu();
popup.add(new JScrollPane(list));
popup.setFocusable(false);
final JTextField field = new JTextField(20);
Action down = new AbstractAction("nextElement") {
#Override
public void actionPerformed(ActionEvent e) {
int next = Math.min(list.getSelectedIndex() + 1,
list.getModel().getSize() - 1);
list.setSelectedIndex(next);
list.ensureIndexIsVisible(next);
}
};
field.getActionMap().put("nextElement", down);
field.getInputMap().put(
KeyStroke.getKeyStroke("DOWN"), "nextElement");
As your context is very similar to a JComboBox, you might consider having a look into the sources of BasicComboBoxUI and BasicComboPopup.
Edit
Just for fun, the following is not answering the focus question :-) Instead, it demonstrates how to use a sortable/filterable JXList to show only the options in the dropdown which correspond to the typed text (here with a starts-with rule)
// instantiate a sortable JXList
final JXList list = new JXList(Locale.getAvailableLocales(), true);
list.setSortOrder(SortOrder.ASCENDING);
final JPopupMenu popup = new JPopupMenu();
popup.add(new JScrollPane(list));
popup.setFocusable(false);
final JTextField field = new JTextField(20);
// instantiate a PatternModel to map text --> pattern
final PatternModel model = new PatternModel();
model.setMatchRule(PatternModel.MATCH_RULE_STARTSWITH);
// listener which to update the list's RowFilter on changes to the model's pattern property
PropertyChangeListener modelListener = new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if ("pattern".equals(evt.getPropertyName())) {
updateFilter((Pattern) evt.getNewValue());
}
}
private void updateFilter(Pattern newValue) {
RowFilter<Object, Integer> filter = null;
if (newValue != null) {
filter = RowFilters.regexFilter(newValue);
}
list.setRowFilter(filter);
}
};
model.addPropertyChangeListener(modelListener);
// DocumentListener to update the model's rawtext property on changes to the field
DocumentListener documentListener = new DocumentListener() {
#Override
public void removeUpdate(DocumentEvent e) {
updateAfterDocumentChange();
}
#Override
public void insertUpdate(DocumentEvent e) {
updateAfterDocumentChange();
}
private void updateAfterDocumentChange() {
if (!popup.isVisible()) {
popup.show(field, 0, field.getHeight());
}
model.setRawText(field.getText());
}
#Override
public void changedUpdate(DocumentEvent e) {
}
};
field.getDocument().addDocumentListener(documentListener);
It looks straight forward to me. Add the following
field.requestFocus();
after
menu.add(list);
menu.show(field, 0, field.getHeight());
Of course, you will have to code for when to hide the popup etc based on what is going on with the JTextField.
i.e;
menu.show(field, field.getX(), field.getY()+field.getHeight());
menu.setVisible(true);
field.requestFocus();
You may take a look to JXSearchField, which is part of xswingx