SWTのメニュー定義がめんどくさい。

SWTのメニュー

普通にSWTでメニューバーを作ろうとすると、例えばこんな感じになります。

http://git.eclipse.org/c/platform/eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet29.java

// メニューバーの作成とウインドウ(Shell)への設定
Menu bar = new Menu (shell, SWT.BAR);
shell.setMenuBar (bar);
// "File"メニューの作成
MenuItem fileItem = new MenuItem (bar, SWT.CASCADE);
fileItem.setText ("&File");
Menu submenu = new Menu (shell, SWT.DROP_DOWN);
fileItem.setMenu (submenu);
// File下の"Select All"メニューの作成
MenuItem item = new MenuItem (submenu, SWT.PUSH);
item.addListener (SWT.Selection, (e)->System.out.println ("Select All"));
item.setText ("Select &All\tCtrl+A");
item.setAccelerator (SWT.MOD1 + 'A');

元のソースに対して、コメントを追加し、メニュー選択時処理をラムダで定義しましたが、本質的には同じです。

  • "File"の作成に4行(ラベル作成2行、内包設定2行)
  • "Select All"作成に4行

これは長い。メニューで通常求められる要素は限定的なので、その範囲で済むならシンプルにしたいところです。

そこで、基本的に1項目1行で作成できるように補助クラスを作ってみます。「他を内包する項目」と「最終的に選ばれる項目」それぞれをクラスにして、構造を定義して、最後に一気にMenuやMenuItemクラスのインスタンスを構築する線でいきます。また、Accelerator(ショートカットキー)はsetAcceleratorとsetText後半が被るので、Acceleratorからテキストラベルを設定することで設定を簡略化してみます。

// 一気に構築するための共通インタフェース
public interface MenuElement {
    void build(MenuFolder parent);
    String getLabel();
}
// 最終的に選ぶ項目
public class MenuButton implements MenuElement {
    private String myLabel;
    private int myAccelerator;
    private MenuItem myMenuItem;
    private Listener myListener;

    public MenuButton(String label) {
        this(label, 0, null);
    }
    
    public MenuButton(String label, int accelerator) {
        this(label, accelerator, null);
    }
    
    public MenuButton(String label, int accelerator, Listener listener) {
        myLabel = label;
        myAccelerator = accelerator;
        myListener = listener;
    }
    
    public MenuItem getMenuItem() {
        return myMenuItem;
    }
    
    public String getLabel() {
        return myLabel;
    }
    
    public void build(MenuFolder parent) {
        myMenuItem = new MenuItem(parent.getMenu(), SWT.PUSH);
        String label = myLabel;
        
        if (myAccelerator > 0) {
            myMenuItem.setAccelerator(myAccelerator);
            label += "\t";
            if ((myAccelerator & SWT.CTRL) != 0) {
                label += "Ctrl+";
            }
            if ((myAccelerator & SWT.SHIFT) != 0) {
                label += "Shift+";
            }
            if ((myAccelerator & SWT.ALT) != 0) {
                label += "Alt+";
            }
            label += String.valueOf((char)(myAccelerator & (~SWT.MODIFIER_MASK)));
        }
        
        if (myListener != null) {
            myMenuItem.addListener(SWT.Selection, myListener);
        }
        
        myMenuItem.setText(label);
    }
}
// 他を内包する項目(名前はもっと違った方がよかったかも)
public class MenuFolder implements MenuElement {
    private String myLabel;
    private MenuItem myMenuItem;
    private Menu myMenu;
    private List<MenuElement> myItems;
    
    public static MenuFolder makeRootMenu() {
        return new MenuFolder();
    }
    
    private MenuFolder() {
        this("");
    }
    
    public MenuFolder(String label) {
        myLabel = label;
        init();
    }
    
    private void init() {
        myItems = new ArrayList<MenuElement>();
    }

    public MenuFolder addFolder(String label) {
        MenuFolder child = new MenuFolder(label);
        myItems.add(child);
        return child;
    }
    
    public MenuFolder addButton(String label) {
        return addButton(label, 0, null);
    }
    
    public MenuFolder addButton(String label, int accelerator) {
        return addButton(label, accelerator, null);
    }
    
    public MenuFolder addButton(String label, int accelerator, Listener listener) {
        MenuButton child = new MenuButton(label,accelerator,listener);
        myItems.add(child);
        return this;
    }
    
    public Menu getMenu() {
        return myMenu;
    }
    
    public String getLabel() {
        return myLabel;
    }

    public void buildMenuBar(Shell parent) throws IllegalStateException {
        if (myLabel != null && myLabel.length()>0) {
            throw new IllegalStateException("this is not root menu!");
        }
        if (myItems == null || myItems.size() < 1) {
            throw new IllegalStateException("no child");
        }
        
        myMenu = new Menu(parent,SWT.BAR);
        parent.setMenuBar(myMenu);
        
        for (MenuElement element : myItems) {
            element.build(this);
        }
    }
    
    public void build(MenuFolder parent) {
        myMenuItem = new MenuItem(parent.getMenu(), SWT.CASCADE);
        myMenuItem.setText(myLabel);
        if (myItems == null || myItems.size() < 1) return;
        
        myMenu = new Menu(myMenuItem);
        myMenuItem.setMenu(myMenu);
        
        for (MenuElement element : myItems) {
            element.build(this);
        }
    }
    
    public MenuFolder getSubFolder(String label) throws IllegalArgumentException {
        for (MenuElement element : myItems) {
            if (element instanceof MenuFolder) {
                MenuFolder f = (MenuFolder)element;
                if (f.getLabel().equals(label)) return f;
            }
        }
        throw new IllegalArgumentException("no item found");
    }
    
    public MenuElement getElement(String label) throws IllegalArgumentException {
        for (MenuElement element : myItems) {
            if (element.getLabel().equals(label)) return element;
        }
        throw new IllegalArgumentException("no item found");
    }
}

これらを使った場合のメニュー作成は、次のようになります。

MenuFolder menuBar = MenuFolder.makeRootMenu();
menuBar.addFolder("&File")
.addButton("Select &All", SWT.MOD1 + 'A', (e)->System.out.println ("Select All"));
menuBar.buildMenuBar(myShell);

1項目1行なので、ソースからメニュー構造がわかると思います。ここではラジオボタン式メニューなどのクラスを設定していませんが、その辺も同様に作れるのではないかと思っています。