| | 801 | /** |
| | 802 | * Create spacer: empty component with fixed width. |
| | 803 | */ |
| | 804 | private JComponent createSpacer(int pixels) { |
| | 805 | JTextPane spacer = new JTextPane(); |
| | 806 | spacer.setMinimumSize(new Dimension(pixels, 0)); |
| | 807 | spacer.setMaximumSize(new Dimension(pixels, 0)); |
| | 808 | return spacer; |
| | 809 | } |
| | 810 | |
| | 811 | /** |
| | 812 | * Create search field. |
| | 813 | */ |
| | 814 | private JComponent createSearchField() { |
| | 815 | JTextField searchField = new JTextField(); |
| | 816 | searchField.setEditable(true); |
| | 817 | searchField.setMaximumSize(new Dimension(200, 50)); |
| | 818 | searchField.setToolTipText(marktr("Search menu items")); |
| | 819 | searchField.addKeyListener(new SearchFieldKeyListener()); |
| | 820 | searchField.getDocument().addDocumentListener(new SearchFieldTextListener(this, searchField)); |
| | 821 | return searchField; |
| | 822 | } |
| | 823 | |
| | 824 | /** |
| | 825 | * Search main menu for items with {@param textToFind} in title. |
| | 826 | * @param textToFind |
| | 827 | * @return not null list of found menu items. |
| | 828 | */ |
| | 829 | private List<JMenuItem> findMenuItems(String textToFind) { |
| | 830 | textToFind = textToFind.toLowerCase(); |
| | 831 | LinkedList<JMenuItem> result = new LinkedList<>(); |
| | 832 | int menuCount = getMenuCount(); |
| | 833 | |
| | 834 | //Iterate over main menus |
| | 835 | for(MenuElement menuElement : getSubElements()) { |
| | 836 | if( !(menuElement instanceof JMenu) ) continue; |
| | 837 | |
| | 838 | JMenu mainMenuItem = (JMenu) menuElement; |
| | 839 | if(mainMenuItem.getAction()!=null && mainMenuItem.getText().toLowerCase().contains(textToFind)) { |
| | 840 | result.add(mainMenuItem); |
| | 841 | } |
| | 842 | |
| | 843 | //Search recursively |
| | 844 | findMenuItems(mainMenuItem, textToFind, result); |
| | 845 | } |
| | 846 | return result; |
| | 847 | } |
| | 848 | |
| | 849 | /** |
| | 850 | * Recursive walker for menu items. Only menu items with action are selected. If menu item |
| | 851 | * contains {@param textToFind} it's appended to result. |
| | 852 | * @param menu |
| | 853 | * @param textToFind |
| | 854 | * @param result |
| | 855 | */ |
| | 856 | private void findMenuItems(final JMenu menu, final String textToFind, final List<JMenuItem> result) { |
| | 857 | int count = menu.getItemCount(); |
| | 858 | for(int i=0; i<count; i++) { |
| | 859 | JMenuItem menuItem = menu.getItem(i); |
| | 860 | if(menuItem == null) continue; |
| | 861 | |
| | 862 | if (menuItem.getAction()!=null && menuItem.getText().toLowerCase().contains(textToFind)) { |
| | 863 | result.add(menuItem); |
| | 864 | } |
| | 865 | |
| | 866 | //Go recursive if needed |
| | 867 | if(menuItem instanceof JMenu) { |
| | 868 | findMenuItems((JMenu) menuItem, textToFind, result); |
| | 869 | } |
| | 870 | } |
| | 871 | } |
| | 872 | |
| | 920 | |
| | 921 | /** |
| | 922 | * This listener is designed to handle ENTER key pressed in menu search field. |
| | 923 | * When user presses Enter key then selected item of "searchResultsMenu" is triggered. |
| | 924 | */ |
| | 925 | class SearchFieldKeyListener implements KeyListener { |
| | 926 | public SearchFieldKeyListener() { |
| | 927 | super(); |
| | 928 | } |
| | 929 | |
| | 930 | @Override |
| | 931 | public void keyPressed(KeyEvent e) { |
| | 932 | if(e.getKeyCode() == KeyEvent.VK_ENTER) { |
| | 933 | //On ENTER selected menu item must be triggered |
| | 934 | MenuElement[] selection = MenuSelectionManager.defaultManager().getSelectedPath(); |
| | 935 | if(selection.length > 1) { |
| | 936 | MenuElement selectedElement = selection[selection.length-1]; |
| | 937 | if(selectedElement instanceof JMenuItem) { |
| | 938 | JMenuItem selectedItem = (JMenuItem) selectedElement; |
| | 939 | Action menuAction = selectedItem.getAction(); |
| | 940 | menuAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null)); |
| | 941 | e.consume(); |
| | 942 | } |
| | 943 | } |
| | 944 | } |
| | 945 | } |
| | 946 | |
| | 947 | @Override |
| | 948 | public void keyTyped(KeyEvent e) { } |
| | 949 | |
| | 950 | @Override |
| | 951 | public void keyReleased(KeyEvent e) { } |
| | 952 | } |
| | 953 | |
| | 954 | class SearchFieldTextListener implements DocumentListener { |
| | 955 | private final JTextField searchField; |
| | 956 | private final MainMenu mainMenu; |
| | 957 | private String currentSearchText = null; |
| | 958 | |
| | 959 | public SearchFieldTextListener(MainMenu mainMenu, JTextField searchField) { |
| | 960 | this.mainMenu = mainMenu; |
| | 961 | this.searchField = searchField; |
| | 962 | } |
| | 963 | |
| | 964 | @Override |
| | 965 | public void insertUpdate(DocumentEvent e) { |
| | 966 | doSearch(searchField.getText()); |
| | 967 | } |
| | 968 | |
| | 969 | @Override |
| | 970 | public void removeUpdate(DocumentEvent e) { |
| | 971 | doSearch(searchField.getText()); |
| | 972 | } |
| | 973 | |
| | 974 | @Override |
| | 975 | public void changedUpdate(DocumentEvent e) { |
| | 976 | doSearch(searchField.getText()); |
| | 977 | } |
| | 978 | |
| | 979 | //TODO: perform some delay (maybe 200 ms) before actual searching. |
| | 980 | void doSearch(String searchTerm) { |
| | 981 | searchTerm = searchTerm.trim().toLowerCase(); |
| | 982 | |
| | 983 | if (searchTerm.equals(currentSearchText)) { |
| | 984 | return; |
| | 985 | } |
| | 986 | currentSearchText = searchTerm; |
| | 987 | if (searchTerm.length() == 0) { |
| | 988 | //No text to search |
| | 989 | hideMenu(); |
| | 990 | return; |
| | 991 | } |
| | 992 | |
| | 993 | List<JMenuItem> searchResult = mainMenu.findMenuItems(currentSearchText); |
| | 994 | if(searchResult.size() == 0) { |
| | 995 | //Nothing found |
| | 996 | hideMenu(); |
| | 997 | return; |
| | 998 | } |
| | 999 | |
| | 1000 | if(searchResult.size() > 20) { |
| | 1001 | //Too many items found... |
| | 1002 | searchResult = searchResult.subList(0, 20); |
| | 1003 | } |
| | 1004 | |
| | 1005 | //Update Popup menu |
| | 1006 | searchResultsMenu.removeAll(); |
| | 1007 | for (JMenuItem foundItem : searchResult) { |
| | 1008 | searchResultsMenu.add(foundItem.getText()).setAction(foundItem.getAction()); |
| | 1009 | } |
| | 1010 | searchResultsMenu.pack(); |
| | 1011 | searchResultsMenu.show(mainMenu, searchField.getX(), searchField.getY() + searchField.getHeight()); //Put menu right under search field |
| | 1012 | |
| | 1013 | searchField.grabFocus(); //This is tricky. User still is able to edit search text. While Up and Down keys are handled by Popup Menu. |
| | 1014 | } |
| | 1015 | |
| | 1016 | private void hideMenu() { |
| | 1017 | searchResultsMenu.setVisible(false); |
| | 1018 | } |
| | 1019 | |
| | 1020 | } |