/**
* 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();
}