ポップアップメニュー生成用のクラス

ここで、紹介するJMenuStringは、 キー操作で 例えば次のようなポップアップメニューを作るためのクラスとして作りました。

メニューは、表示中のテキストの内容とキャレット位置から自動的に必要なメニュー項目だけを、 出現させる目標で作りました。 次のクラス図になっています。

JMenuString
+String item;//メニュー表示用の文字列
+int level;//表示する際のメニューレベル
+String match;//前方サーチ適合文字列
+String matchScope;//前方サーチ範囲指定文字列
+int ansKeyCode;//応答するキーコード
+JMenuString readMenuString (String s) 1行のsからJMenuStringを生成して返す。
+ArrayList <JMenuString> readMenuString_s(String str)
  strの複数行からJMenuString集合を生成して返す。
+JPopupMenu getPopupMenu(ArrayList <JMenuString> lst, String txt, int keyCode,ActionListener listener)
  txtの最後にキャレットがあるとして、keyCodeの仮想キーコード入力で必要なPopupMenuを生成して返す。

使い方の概要
複数のJMenuStringを生成するためのstrから、ArrayList <JMenuString> のlstを得て、 それでJPopupMenuを作ってメニュー表示に使います。
まず、ここで使う str を使った例を示します。
String htmlMenuStr0=
	"<html></html>,,<html,44,重複使用なし\n"+
	"<head></head>,,<head,44,重複使用なし\n"+
	"<body></body>,,<body,44,重複使用なし\n"+
	"<title></title>,<head,</head>,44\n"+
	"<style type=\"text/css\">body { background-color: #FFEEBB;}</style>,<head,</head>,44\n"+
	"<script language=\"javascript\"></script>,<head,</head>,44\n"+
	"<h1></h1>,<body,</body>,44\n"+
	"<h2></h2>,<body,</body>,44\n"+
	"<p></p>,<body,</body>,44\n"+
	"<hr>,<body,</body>,44,\n"+
	"インライン要素,<body,</body>,44\n"+
	"\t<br>,<body,</body>,44,\n"+
	"\t<img src=\"\">,<body,</body>,44\n"+
	"\t<span></span>,<body,</body>,44\n"+
	"\t<a href=\"\"></a>,<body,</body>,44\n"+
	"width=,<img,>,\n"+
	"height=,<img,>,\n"+	
	"特殊文字,<body,</body>,54\n"+
	"\t&nbsp;,<body,</body>,54\n"+
	"\t&lt;,<body,</body>,54\n"+
	"\t&gt;,<body,</body>,54\n"+
	"\t&amp;,<body,</body>,54\n";

	ArrayList <JMenuString> mneuStringList=JMenuString.readMenuString_s(htmlMenuStr0);

上記で、JMenuStringに対応する1行のデータは、コンマで(1),(2),(3),(4),(5)と区切り、次の意味にしている。
(1)メニュー表示項目(この文字例先頭がタブの場合、サブメニューを意味する。)
(2)先方にあるべき文字列(これが空の場合、既に使ったJMenuStringを重複して使わないと言う意味)
(3)後方にあるべき文字列((2)が空で、これが存在している場合は、メニューに登録しない)
(4)仮想キーコード(複数のキーに対応するサブメニューの場合、0の設定が可能でこれは任意の文字を意味する。)
(5)"重複使用なし"の文字列記載箇所があるが、この部分はコメントでプログラム的には利用していない。

これらは、順次にJMenuStringのインスタンス変数に記憶して使います。
最終的に上記で得られたJMenuStringのインスタンス集合管理するmneuStringListで、
次のようにポップアップメニューを生成して使います。
これは、メニューをポップアップさせるJTextPaneの派生クラス内キープレスイベントで使う例です。
そして登録は、一つ前の要素(prevMenuItem)を、その確定時に行います。
	public void keyPressed(KeyEvent e){//キーを押しているときに呼び出されます。
		int pos = this.getSelectionStart();//選択している位置を取得
		Rectangle rect = this.modelToView(pos);//カーソルの座標取得
		String txt = this.document.getText(0, pos);//先頭からキャレットまでの文字列
		JPopupMenu popupMenu = JMenuString.getPopupMenu(HtmlEditor.mneuStringList, txt, keyCode, this);
		if(popupMenu != null){
			popupMenu.show(this, rect.x, rect.y);
		}
		//・・・・・
	}
以下に、このクラスの要であるgetPopupMenuメソッドの流れを示します。
このメソッは、第1引数のHtmlEditor.mneuStringListにあるJMenuStringインスタンス集合から、順番に必要と判断された時だけ JPopupMenuに追加登録する。必要かどうかの判断は、JMenuStringインスタンスと、第1引数のtxtと、第3引数のkeyCodeです。
また、第4引き数thisは、メニュー処理のイベントを処理するActionListenerをインプリメントしたインスタンスです。以下で メソッドのヘッダー部と引数の概要、流れ図を示します。
なお、メニュー項目を持たないサブメニュー(JMenu)を表示しません。 またサブメニューが入るメニュー項目は、そのサブメニューが並ぶ要素の直前に位置するメニュー項目です。 よって、メニュー項目をサブメニュー入れた終わった時点(階層レベルが小さくなった時)で登録します。 そのため、サブメニューをスタックに入れて管理しています。
public static JPopupMenu getPopupMenu(ArrayList  lst, String txt, int keyCode,ActionListener listener)
 lst:メニュー項目(JMenuString)のリストを作って、指定します。予め、JMenuString.readMenuString_sなどで用意しておきます。
 txt:テキスト編集コンポーネントで、先頭からキャレットまでの文字列です。この内容からマッチするメニュー項目を選択登録します。
 keyCode:メニュー項目(JMenuString)のインスタンス変数ansKeyCodeが、このキーコードに一致する場合に、そのメニュー項目が登録対象となります。
 listener:lst内の各メニュー項目(JMenuString)に設定するインスタンス。それはメニュー選択のActionListenerをimplementsしたインスタンスです。
getPopupMenuのスタート
lst内の全てメニュー項目(JMenuString)に登録される以前の全てのリスナーを削除し、引数のリスナーを登録する
  (繰り返しで各メニュー項目(JMenuString)に対して行われる。リスナーの削除でも繰り返しを使う)
popupMenu = new JPopupMenu();で作り、これに必要なJMenuStringを登録して、最後で戻り値にする。
Stack stack = new Stack();でサブメニューを記憶するスタック(空でなければ登録)   
JMenuString prevMenuItem = null; 繰り返しで、前の登録要素を管理する。前の要素なので確定
for(JMenuString menuStr : lst) で、lstの各要素をmenuStrにセットして、それを登録する繰り返し
 サブメニューを登録するか?(スタックにデータがあって、そのレベルがmenuStrのレベルより小さい?)
   後方判定繰り返し(スタックに詰まれるサブメニューを登録する繰り返し)
    prevMenuItemのレベルがスタックサイズと同じか?
     prevMenuItemをスタックトップのメニュー項目に入れる。prevMenuItemはnullへ
    スタックトップのサブメニュー内にメニュー項目があか?
     スタックトップを取り出して、減ったスタックトップのサブメニューのメニュー項目へセット
   menuStrのレベルがスタックサイズより小さければ、上記を繰り返す。
 prevMenuItem が null でない? (その場合は以下でprevMenuItemを登録する。)
  prevMenuItemのレベルより、menuStrのメニューレベルが大きい?
   prevMenuItemをサブメニューとしてスタックに積む。
  スタックが空でない? (上記の繰り返しで、prevMenuItemのレベルとスタックサイズは同じに調整されている)
   スタックトップのサブメニューへprevMenuItemのメニュー項目を追加
  いずれでもない?
   トップメニューとしてprevMenuItemのメニュー項目を追加
  prevMenuItem=null; 登録終わったという設定
menuStr.ansKeyCodeのキーコードが、引数のキーコードとマッチしない?
 スキップ(menuStrは登録対象でない)
重複不可のitemが、引数txtの中で、すでに使っていると判断された?
 スキップ(menuStrは登録対象でない)
引数txtがmenuStr.match〜menuStr.matchScopeの範囲内でないと判断された?
 スキップ(menuStrは登録対象でない)
prevMenuItem = menuStr; menuStrのメニュー項目が適合したので、次の繰り返しで登録するとした設定
スタックにサブメニューがあるか?
  後方判定繰り返し(スタックに詰まれるサブメニューを登録する繰り返し)
   prevMenuItemのレベルがスタックサイズと同じか?
    prevMenuItemをスタックトップのメニュー項目に入れる。prevMenuItemはnullへ
   スタックトップのサブメニュー内にメニュー項目があか?
    スタックトップを取り出して、減ったスタックトップのサブメニューのメニュー項目へセット
  スタックサイズがあれば、上記を繰り返す。
prevMenuItem != null で、まだ登録されていないなら?
 prevMenuItemをトップメニューとして登録
popupMenuにメニュー項目がない?
 return null;
return >popupMenu; で構築されたポップアップメニューを返す。

以下で、上記クラスと確認用のクラスのソースを示します。 なおここで使っているデータファイルHtmlEditorMenu.txtのリンクは、ここに示します。
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event*;

//----------------------------------------------------------------------------
//階層構造を持つポップアップメニュー項目を作るためのメニュー項目用クラス
public class JMenuString extends JMenuItem {
	static final long serialVersionUID = 100009;
	String item;//メニュー表示用の文字列
	int level;//表示する際のメニューレベル
	String match;//前方サーチ適合文字列
	String matchScope;//前方サーチ範囲指定文字列
	int ansKeyCode;//応答するキーコード

	// 1行ずつ読み取り、それが、'\t'が先頭にあれば再帰する。
	public static JMenuString readMenuString (String s) {
		if(s == null) return null;
		String []a = s.split(",");
		if(a.length == 0) {
			System.out.println("メニュー文字列に矛盾があります。"); 
		}
		
		//2012-3-2 コンマのデータを 、「\\,」の表記で使えるように変更する。また「\\n」を改行を使えるようにする。
		for(int i=0; i< a.length;i++){
			if(a[i].endsWith("\\\\")){
				int k=i;
				while(a[i].endsWith("\\\\")){//「\\,」を,のデータに戻して、次の要素を繋げる
					a[i] = a[i].substring(0, a[i].length()-2) + "," + a[i+1];
					// a[i+1] の要素を削除する
					List<String> list = new ArrayList<String>(Arrays.asList(a));
					list.remove(i+1);
					a = (String [])list.toArray(new String[list.size()]);
				}
				i=k;
			}
		}
		
		JMenuString menuString = new JMenuString();
		
		// a[0]のメニュー表示文字列のレベルを調べて設定
		int n = 0;
		while(a[0].charAt(n++) == '\t');
		menuString.level = n-1;//タブの数を設定

		try {
			menuString.item = a[0].trim().replaceAll("\\\\\\\\n", "\n");//メニュー項目 2012-3-2 改行データを使える置き換えに変更
			menuString.match = a[1];
			menuString.matchScope = a[2];
			menuString.ansKeyCode = Integer.parseInt(a[3].trim());
		}
		catch(Exception e){
			System.err.println("----- Error JMenuString data :" + menuString.item);
		}
		menuString.setText(menuString.item);//メニュー項目として表示する文字列セット
		return menuString;
	}
	
	// str から、メニューデータを1行分ずつ読み取り、ArrayListに入れ、それを返す。
	public static ArrayList <JMenuString> readMenuString_s(String str) {
		ArrayList <JMenuString> mneuStringList = new ArrayList <JMenuString>();
		String []a = str.split("\n");
		JMenuString menuStr;
		for(int i = 0; i < a.length; i++){
			if(a[i].startsWith("//")) continue; //コメントと判断して使わない
			if(a[i].trim().equals(""))continue;
			menuStr=readMenuString(a[i]);
			mneuStringList.add(menuStr);
		}
		return mneuStringList;
	}
	
	//仮想キーコード(keyCode)で出すポップアップメニュー用のJPopupMenuを、BaseMenuItemの集合から、txtに内容に合うメニュー項目で生成する。
	public static JPopupMenu getPopupMenu(ArrayList <JMenuString> lst, String txt, int keyCode, ActionListener listener){
		if(lst == null || keyCode < MenuKeyEvent.VK_SPACE || keyCode > MenuKeyEvent.VK_Z) return null;

		//lst内の全てメニュー項目(JMenuString)に登録される以前の全てのリスナーを削除し、引数のリスナーを登録する
		for(JMenuString jmstr : lst){
			ActionListener []act = jmstr.getActionListeners();
			if(act.length > 0 && act[0] == listener) break;
			for(int j = 0 ; j < act.length; j++){
				jmstr.removeActionListener(act[j]); //以前のリスナーを削除
			}
			jmstr.addActionListener(listener);//新しいリスナーを登録
		}	
		

		JPopupMenu popupMenu = new JPopupMenu();//戻り値管理用
		Stack<JMenu> stack = new Stack<JMenu>();
		JMenuString prevMenuItem = null;

		
		for(JMenuString menuStr : lst){// この順番で、リスト項目を追加する。
//			System.out.printf("%s:%d,%d\n", menuStr.item,menuStr.ansKeyCode, keyCode);
			
			if(stack.isEmpty()==false && stack.size() >  menuStr.level){
				do{
					if(prevMenuItem != null && prevMenuItem.level == stack.size()) {
						stack.peek().add(prevMenuItem);
						prevMenuItem = null;
					}
					JMenu submenu = stack.pop();
					if(submenu.getSubElements().length > 0){
						if(stack.size() == 0) {
							popupMenu.add(submenu);//トップへ、メニュー登録
							break;
						}
						stack.peek().add(submenu);//サブへ、メニュー登録
					}
				}while(stack.size() >  menuStr.level);
			}
			
			if(prevMenuItem != null){
				if(prevMenuItem.level <  menuStr.level){//前より、メニューレベルが大きい
					stack.push(new JMenu(prevMenuItem.item));//サブメニューを用意
				} else if(stack.isEmpty()==false){
					stack.peek().add(prevMenuItem);
				} else {
					popupMenu.add(prevMenuItem);//トップへ、メニュー登録
				}
				prevMenuItem = null;
			}

			if(menuStr.ansKeyCode != 0 && menuStr.ansKeyCode != keyCode){
				continue;//操作キーが、このメニュー項目用でなければ、登録しない。
			}
			
			int iRangeEnd = -1;//txtがmatchScopeと適合するかの判定用
			if(menuStr.matchScope.equals("") == false){
				iRangeEnd = txt.lastIndexOf(menuStr.matchScope);
				if(iRangeEnd != -1 &&  menuStr.match.equals("") == true){
					continue;	//重複不可のitemで、すでに使っていると判断されたので登録しない
				}
			}

			int iRangeStart = -1;
			if(menuStr.match.equals("")==false){
				iRangeStart = txt.lastIndexOf(menuStr.match);
				if(iRangeStart == -1){
					continue;//範囲先頭がないので適合しない。
				}
				if(iRangeEnd != -1 && iRangeStart < iRangeEnd){
					continue;// 範囲指定があって、適合しない。
				}
			}			
				
			//メニュー項目が適合したので登録する処理
			prevMenuItem = menuStr;
		}
		//繰り返し後の処理
		if(stack.empty() == false) {
			do{
				if(prevMenuItem != null && prevMenuItem.level == stack.size()) {
					stack.peek().add(prevMenuItem);
					prevMenuItem = null;
				}
				JMenu submenu = stack.pop();
				if(submenu.getSubElements().length > 0){
					if(stack.size() == 0) {
						popupMenu.add(submenu);//トップへ、メニュー登録
						break;
					}
					stack.peek().add(submenu);//サブへ、メニュー登録
				}
			}while(stack.size()==0);
		}
		if(prevMenuItem != null){
			popupMenu.add(prevMenuItem);
		}
		if(popupMenu.getSubElements().length == 0) return null;
		return popupMenu;
	}

	//----------------------------------------------------------------------------
	// 以下は、テスト用のクラスと、main   	
	static class TestFrame extends javax.swing.JFrame implements ActionListener,KeyListener {
		javax.swing.JTextPane text = new javax.swing.JTextPane() {
			@Override
			public boolean getScrollableTracksViewportWidth() {
				Object parent = getParent();
				if (parent instanceof javax.swing.JViewport) {
					javax.swing.JViewport port = (javax.swing.JViewport) parent;
					int w = port.getWidth();	// 表示できる範囲(上限)

					javax.swing.plaf.TextUI ui = getUI();
					java.awt.Dimension sz = ui.getPreferredSize(this); // 実際の文字列サイズ
					if (sz.width < w) {
						return true;// 折り返しする
					}
				}
				return false;	// 折り返しを行わない
			}
		};
		javax.swing. JScrollPane scroll = new javax.swing.JScrollPane(text);
		ArrayList <JMenuString> mneuStringList;
		
		public void actionPerformed(java.awt.event.ActionEvent e){
			Object obj = e.getSource();
			try{
				if(obj instanceof JMenuString){
					JMenuString menu = (JMenuString)obj;
					int pos = this.text.getSelectionStart();//選択している位置を取得 
					this.text.getDocument().insertString(pos, menu.item, null);
				}
			}
			catch(Exception err){ }
		}
		public void keyPressed(KeyEvent e){//キーを押しているときに呼び出されます。
        			int keyCode = e.getKeyCode();
    			if(e.isControlDown() == false)return;

    			int pos = this.text.getSelectionStart();//選択している位置を取得
    			try { 
    				java.awt.Rectangle rect = this.text.modelToView(pos);//カーソルの座標取得
    				String txt = this.text.getDocument().getText(0, pos);//先頭からキャレットまでの文字列
    				JPopupMenu popupMenu = JMenuString.getPopupMenu(this.mneuStringList, txt, keyCode, this);
				if(popupMenu != null){
					popupMenu.show(this, rect.x, rect.y);
    				}
    			}
    			catch(Exception err){}
		}
		public void keyReleased(KeyEvent e){//キーを離したときに呼び出されます。 
		}
		public void keyTyped(KeyEvent e){//キーをタイプすると呼び出されます。
		}
		TestFrame(){//テストフレーム用コンストラクタ
			this.setDefaultCloseOperation(javax.swing.JFrame.DISPOSE_ON_CLOSE);
			this.add(scroll);
			scroll.setPreferredSize( new java.awt.Dimension(600,400));
			this.pack();
			this.setVisible(true);		
			String htmlMenuStr0=
				"<html >\\\\n</html>,,<html,44,重複使用なし\n"+
				"<head></head>,,<head,44,重複使用なし\n"+
				"<body></body>,,<body,44,重複使用なし\n"+
				"<title></title>,<head,</head>,44\n"+
				"<h1></h1>,<body,</body>,44\n"+
				"<p></p>,<body,</body>,44\n"+
				"インライン要素,<body,</body>,0\n"+
				"\t<br>,<body,</body>,44,\n"+
				"\t<img src=\"\">,<body,</body>,44\n"+
				"\t<span></span>,<body,</body>,44\n"+
				"\t<a href=\"\"></a>,<body,</body>,44\n"+
				"\twidth=,<img,>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"height=,<img,>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"特殊文字,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"\t&nbsp;,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"\tタグ記号,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"\t\t&lt;,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"\t\t&gt;,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n"+
				"\t&amp;,<body,</body>,"+MenuKeyEvent.VK_SPACE+"\n";

			try {
				File file = new File("HtmlEditorMenu.txt");
				FileInputStream fis = new FileInputStream(file);
				byte []ba = new byte[(int)file.length()];
				fis.read(ba);
				fis.close();
				htmlMenuStr0 = new String(ba);
			}
			catch(Exception e){	}
			
			mneuStringList=JMenuString.readMenuString_s(htmlMenuStr0);
			this.text.addKeyListener(this);
		}
	}
	public static void main(String []a){
		new TestFrame();
	}
}