/** * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.codesourcery.jasm16.ide.ui; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.SwingUtilities; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; public abstract class MenuManager { // @GuardedBy( entries ) private final List<MenuEntry> entriesList = new ArrayList<MenuEntry>(); private final Object menuBarLock = new Object(); // @GuardedBy( menuBarLock ) private JMenuBar menuBar; // @GuardedBy( menuBarLock ) private ItemWatchdog watchdogThread; private static final class MenuPath { private final String[] path; public MenuPath(String path) { this.path = path.split("/"); } public int length() { return path.length; } public MenuPath[] getAllPaths() { final List<MenuPath> result = new ArrayList<MenuPath>(); for ( int len = 1 ; len <= path.length ; len++ ) { result.add( new MenuPath( (String[]) ArrayUtils.subarray( this.path , 0 , len ) ) ); } return result.toArray( new MenuPath[ result.size() ] ); } @Override public int hashCode() { return toString().hashCode(); } @Override public boolean equals(Object obj) { if ( obj == this ) { return true; } if ( obj instanceof MenuPath) { MenuPath that = (MenuPath) obj; if ( this.path.length != that.path.length ) { return false; } for ( int i = 0 ; i < this.path.length ; i++ ) { if ( ! this.path[i].equals( that.path[i] ) ) { return false; } } return true; } return false; } public MenuPath(String[] path) { this.path = path; } public String getLastPathComponent() { return path[ path.length - 1 ]; } public String toString() { return StringUtils.join( path , "/" ); } public MenuPath getParentPath() { if ( path.length == 1 ) { return null; } return new MenuPath( (String[]) ArrayUtils.subarray( path , 0 , path.length - 1 ) ); } } public static abstract class MenuEntry { private final MenuPath path; private final int mnemonic; private JMenuItem menuItem; public MenuEntry(String path) { this( path , Integer.MAX_VALUE ); } public JMenuItem getMenuItem() { return menuItem; } public void setMenuItem(JMenuItem menuItem) { if (menuItem == null) { throw new IllegalArgumentException("menuItem must not be null"); } this.menuItem = menuItem; } public MenuEntry(String path,int mnemonic) { this.path = new MenuPath( path ); if ( this.path.length() == 1 ) { throw new IllegalArgumentException("Path is too short: "+path); } this.mnemonic = mnemonic; } public boolean hasMnemonic() { return mnemonic != Integer.MAX_VALUE; } public int getMnemonic() { return mnemonic; } public MenuPath getPath() { return path; } public String getParentPath() { MenuPath parentPath = path.getParentPath(); return parentPath == null ? "" : parentPath.toString(); } public String getLabel() { return path.getLastPathComponent(); } public boolean isVisible() { return true; } public boolean isEnabled() { return true; } public abstract void onClick(); } public void addEntry(MenuEntry entry) { if (entry == null) { throw new IllegalArgumentException("entry must not be null"); } synchronized( entriesList ) { this.entriesList.add( entry ); } notifyMenuBarChanged(); } private void notifyMenuBarChanged() { final boolean notifyChange; synchronized( menuBarLock ) { if ( menuBar != null ) { menuBar = null; notifyChange = true; } else { notifyChange = false; } } if ( notifyChange ) { System.out.println("Menu bar has changed!"); menuBarChanged(); } } public void removeEntry(MenuEntry entry) { if (entry == null) { throw new IllegalArgumentException("entry must not be null"); } synchronized( entriesList ) { if ( this.entriesList.remove( entry ) == false ) { return; } } notifyMenuBarChanged(); } public JMenuBar getMenuBar() { synchronized( menuBarLock ) { if ( menuBar == null ) { menuBar = createMenuBar(); if ( watchdogThread == null || ! watchdogThread.isAlive() ) { watchdogThread = new ItemWatchdog(); watchdogThread.start(); } else { watchdogThread.clearCache(); } } return menuBar; } } private class ItemWatchdog extends Thread { // @GuardedBy( stateCache ) private final IdentityHashMap<MenuEntry, Boolean> stateCache = new IdentityHashMap<MenuEntry, Boolean>(); public ItemWatchdog() { setDaemon( true ); setName("menuitem-watchdog-thread"); } public void clearCache() { synchronized(stateCache) { stateCache.clear(); } } @Override public void run() { while( true ) { try { Thread.sleep( 500 ); } catch (InterruptedException e) { } synchronized( menuBarLock ) { if ( menuBar == null ) { continue; } } final List<MenuEntry> entriesCopy; synchronized( entriesList ) { entriesCopy = new ArrayList<MenuEntry>( entriesList ); } boolean cacheModified = false; final Map<MenuEntry, Boolean> cacheCopy; synchronized( stateCache ) { cacheCopy = new IdentityHashMap<MenuEntry, Boolean>( this.stateCache ); } for ( final MenuEntry entry : entriesCopy ) { final Boolean currentState = Boolean.valueOf( entry.isEnabled() ); final Boolean oldState = cacheCopy.get( entry ); if ( oldState == null ) { cacheCopy.put( entry , currentState ); cacheModified = true; } else { if ( ! oldState.equals( currentState ) ) { cacheCopy.put( entry , currentState ); cacheModified = true; SwingUtilities.invokeLater( new Runnable() { @Override public void run() { System.out.println("Item state changed: "+entry.getLabel()+" "+oldState+" -> "+currentState); entry.getMenuItem().setEnabled( currentState.booleanValue() ); } }); } } } if ( cacheModified ) { synchronized( stateCache ) { this.stateCache.clear(); this.stateCache.putAll( cacheCopy ); } } } } } protected JMenuBar createMenuBar() { final List<MenuEntry> copy; synchronized( entriesList ) { copy = new ArrayList<MenuEntry>( entriesList ); } // collect distinct parent paths final List<MenuPath> paths = new ArrayList<MenuPath>(); for ( MenuEntry e : copy ) { if ( ! e.isVisible() ) { continue; } final MenuPath parentPath = e.getPath().getParentPath(); if ( parentPath == null ) { continue; } for ( MenuPath p : parentPath.getAllPaths() ) { if ( ! paths.contains( p ) ) { paths.add( p ); } } } // sort paths ascending by length Collections.sort( paths , new Comparator<MenuPath>() { @Override public int compare(MenuPath o1, MenuPath o2) { final int len1 = o1.toString().length(); final int len2 = o2.toString().length(); if ( len1 < len2 ) { return -1; } else if ( len1 > len2 ) { return 1; } return 0; } }); /* * - a * | * +-- b * | * + c */ // create menu for each path final Map<MenuPath,JMenu> menuesByPath = new HashMap<MenuPath,JMenu>(); for ( MenuPath path : paths ) { final JMenu menu = new JMenu( path.getLastPathComponent() ); menuesByPath.put( path , menu ); JMenu parentMenu = menuesByPath.get( path.getParentPath() ); if ( parentMenu != null ) { parentMenu.add( menu ); } } // setup menu bar //Where the GUI is created: final JMenuBar menuBar = new JMenuBar(); final Set<MenuPath> topLevelMenues = new HashSet<MenuPath>(); for ( final MenuEntry e : copy ) { if ( ! e.isVisible() ) { continue; } final MenuPath parentPath = e.getPath().getParentPath(); final JMenu menu = menuesByPath.get( parentPath ); if ( menu == null ) { throw new RuntimeException("Internal error, failed to create menu for path: "+e.getPath()); } // register top-level menues if ( parentPath.length() == 1 ) { if ( ! topLevelMenues.contains( parentPath ) ) { menuBar.add( menu ); topLevelMenues.add( parentPath ); } } final Action action; action = new AbstractAction( e.getLabel() ) { @Override public void actionPerformed(ActionEvent event) { e.onClick(); } @Override public boolean isEnabled() { return e.isEnabled(); } }; final JMenuItem item = new JMenuItem( action ) { @Override public boolean isEnabled() { return action.isEnabled(); } }; if ( e.hasMnemonic() ) { item.setMnemonic( e.getMnemonic() ); } e.setMenuItem( item ); menu.add( item ); } return menuBar; } public abstract void menuBarChanged(); }