View Javadoc

1   /*
2    * jcurl java curling software framework https://JCurl.mro.name Copyright (C)
3    * 2005-2009 M. Rohrmoser
4    * 
5    * This program is free software; you can redistribute it and/or modify it under
6    * the terms of the GNU General Public License as published by the Free Software
7    * Foundation; either version 2 of the License, or (at your option) any later
8    * version.
9    * 
10   * This program is distributed in the hope that it will be useful, but WITHOUT
11   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12   * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13   * details.
14   * 
15   * You should have received a copy of the GNU General Public License along with
16   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17   * Place, Suite 330, Boston, MA 02111-1307 USA
18   */
19  
20  package org.jcurl.demo.tactics.old;
21  
22  import java.awt.event.ActionEvent;
23  import java.awt.event.InputEvent;
24  import java.awt.event.KeyEvent;
25  import java.lang.annotation.Documented;
26  import java.lang.annotation.ElementType;
27  import java.lang.annotation.Retention;
28  import java.lang.annotation.RetentionPolicy;
29  import java.lang.annotation.Target;
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.util.HashMap;
33  import java.util.Map;
34  import java.util.TreeMap;
35  import java.util.Map.Entry;
36  import java.util.concurrent.Executor;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  import javax.swing.AbstractAction;
41  import javax.swing.Action;
42  import javax.swing.JMenu;
43  import javax.swing.JMenuItem;
44  import javax.swing.KeyStroke;
45  
46  import org.apache.commons.logging.Log;
47  import org.jcurl.core.log.JCLoggerFactory;
48  import org.jcurl.core.ui.TaskExecutor.ForkableFlex;
49  
50  /**
51   * @author <a href="mailto:JCurl@mro.name">M. Rohrmoser </a>
52   * @version $Id$
53   */
54  public class ActionRegistry {
55  	/**
56  	 * Mark a method that should be wrapped into a contextless {@link Action}.
57  	 * 
58  	 * @author <a href="mailto:JCurl@mro.name">M. Rohrmoser </a>
59  	 * @version $Id$
60  	 */
61  	@Target(ElementType.METHOD)
62  	@Retention(RetentionPolicy.RUNTIME)
63  	@Documented
64  	@interface JCAction {
65  		/**
66  		 * Accepts either strings in the form
67  		 * {@link KeyStroke#getKeyStroke(String)} or a simplified form, e.g.
68  		 * <code>CTRL-SHIFT-ALT-F1</code>.
69  		 */
70  		String accelerator() default "";
71  
72  		/** Sorting order for menu entries. &lt; 0 hides. */
73  		int idx();
74  
75  		/** add a separator before this menu entry. */
76  		boolean separated() default false;
77  
78  		/** Prefix the mnemonic with '&'. */
79  		String title();
80  	}
81  
82  	/**
83  	 * Mark a class which {@link JCAction}-marked methods will become entries.
84  	 * 
85  	 * @author <a href="mailto:JCurl@mro.name">M. Rohrmoser </a>
86  	 * @version $Id$
87  	 */
88  	@Target(ElementType.TYPE)
89  	@Retention(RetentionPolicy.RUNTIME)
90  	@Documented
91  	@interface JCMenu {
92  		/** Prefix the mnemonic with '&'. */
93  		String value();
94  	}
95  
96  	private static final ActionRegistry instance = new ActionRegistry();
97  	private static final Pattern KeyPat = Pattern
98  			.compile("((?:(?:CTRL|ALT|SHIFT)-)*)([1-4a-zA-Z]+)");
99  	private static final Log log = JCLoggerFactory
100 			.getLogger(ActionRegistry.class);
101 	private static final Pattern MneFinder = Pattern.compile(".*[&]([^&]).*");
102 	private static final Pattern MneStripper = Pattern.compile("[&](.)");
103 	private static final Map<String, Integer> str2key = new TreeMap<String, Integer>();
104 
105 	static {
106 		str2key.put("UP", KeyEvent.VK_UP);
107 		str2key.put("DOWN", KeyEvent.VK_DOWN);
108 		str2key.put("LEFT", KeyEvent.VK_LEFT);
109 		str2key.put("RIGHT", KeyEvent.VK_RIGHT);
110 		str2key.put("ADD", KeyEvent.VK_ADD);
111 		str2key.put("SUBTRACT", KeyEvent.VK_SUBTRACT);
112 		str2key.put("PLUS", KeyEvent.VK_PLUS);
113 		str2key.put("MINUS", KeyEvent.VK_MINUS);
114 		str2key.put("HOME", KeyEvent.VK_HOME);
115 		str2key.put("END", KeyEvent.VK_END);
116 		str2key.put("PGUP", KeyEvent.VK_PAGE_UP);
117 		str2key.put("PGDN", KeyEvent.VK_PAGE_DOWN);
118 		str2key.put("F1", KeyEvent.VK_F1);
119 		str2key.put("F2", KeyEvent.VK_F2);
120 		str2key.put("F3", KeyEvent.VK_F3);
121 		str2key.put("F4", KeyEvent.VK_F4);
122 		str2key.put("F5", KeyEvent.VK_F5);
123 		str2key.put("F6", KeyEvent.VK_F6);
124 		str2key.put("F7", KeyEvent.VK_F7);
125 		str2key.put("F8", KeyEvent.VK_F8);
126 		str2key.put("F9", KeyEvent.VK_F9);
127 		str2key.put("F10", KeyEvent.VK_F10);
128 		str2key.put("F11", KeyEvent.VK_F11);
129 		str2key.put("F12", KeyEvent.VK_F12);
130 	}
131 
132 	public static ActionRegistry getInstance() {
133 		return instance;
134 	}
135 
136 	public static void invoke(final Class<?> controller, final String method) {
137 		getInstance().findAction(controller, method).actionPerformed(null);
138 	}
139 
140 	public static void invoke(final Class<?> controller, final String method,
141 			final Class<? extends Executor> exec) {
142 		new ForkableFlex() {
143 			public void run() {
144 				getInstance().findAction(controller, method).actionPerformed(
145 						null);
146 			}
147 		}.fork(exec);
148 	}
149 
150 	/** Resolve class (controller) to action. */
151 	private final Map<Class<?>, Map<Method, Action>> c2a = new HashMap<Class<?>, Map<Method, Action>>();
152 	/** Resolve instance (controller) to action. */
153 	private final Map<Object, Map<Method, Action>> i2a = new HashMap<Object, Map<Method, Action>>();
154 
155 	private void addMenuItem(final Object controller, final JMenu ret,
156 			final Method m) {
157 		final JCAction a = m.getAnnotation(JCAction.class);
158 		if (a.separated())
159 			ret.addSeparator();
160 		final JMenuItem mi = new JMenuItem();
161 		mi.setAction(findAction(controller, m));
162 		{
163 			// TODO move to createAction
164 			final Character mne = findMnemonic(a.title());
165 			if (mne != null)
166 				mi.setMnemonic(mne);
167 		}
168 		ret.add(mi);
169 	}
170 
171 	/** Create a disabled {@link Action}. */
172 	private Action createAction(final Object controller, final Method m,
173 			final JCAction a) {
174 		final Action ac = new AbstractAction() {
175 			private static final long serialVersionUID = 2349356576661476730L;
176 
177 			public void actionPerformed(final ActionEvent e) {
178 				try {
179 					m.invoke(controller, (Object[]) null);
180 				} catch (final IllegalArgumentException e1) {
181 					throw new RuntimeException("Unhandled", e1);
182 				} catch (final IllegalAccessException e1) {
183 					throw new RuntimeException("Unhandled", e1);
184 				} catch (final InvocationTargetException e1) {
185 					throw new RuntimeException("Unhandled", e1);
186 				}
187 			}
188 		};
189 		ac.putValue(Action.NAME, stripMnemonic(a.title()));
190 		ac.putValue(Action.ACCELERATOR_KEY, findAccelerator(a.accelerator()));
191 		if (false) {
192 			final Character mne = findMnemonic(a.title());
193 			if (mne != null)
194 				ac.putValue(Action.MNEMONIC_KEY, KeyStroke.getKeyStroke(mne));
195 		}
196 		ac.setEnabled(false);
197 		return ac;
198 	}
199 
200 	public JMenu createJMenu(final Object controller) {
201 		final Class<?> clz = controller.getClass();
202 		final JCMenu ca = clz.getAnnotation(JCMenu.class);
203 		if (ca == null)
204 			return null;
205 		// first get all candidates
206 		final Map<Integer, Method> sorted = new TreeMap<Integer, Method>();
207 		for (final Method me : clz.getMethods()) {
208 			final JCAction ma = me.getAnnotation(JCAction.class);
209 			if (ma == null || ma.idx() < 0)
210 				continue;
211 			if (sorted.put(ma.idx(), me) != null)
212 				throw new IllegalStateException(ma.idx() + ": " + me.toString());
213 		}
214 		// create the menu
215 		final JMenu ret = new JMenu(stripMnemonic(ca.value()));
216 		{
217 			final Character mne = findMnemonic(ca.value());
218 			if (mne != null)
219 				ret.setMnemonic(mne);
220 		}
221 		// add the menu items in natural order
222 		for (final Entry<Integer, Method> elem : sorted.entrySet())
223 			addMenuItem(controller, ret, elem.getValue());
224 		return ret;
225 	}
226 
227 	KeyStroke findAccelerator(final String acc) {
228 		if (acc == null || "".equals(acc))
229 			return null;
230 		final Matcher m = KeyPat.matcher(acc);
231 		if (m.matches()) {
232 			int modifiers = 0;
233 			{
234 				final String gr = m.group(1);
235 				if (!"".equals(gr)) {
236 					final String[] mod = gr.split("-");
237 					for (final String mm : mod)
238 						if ("CTRL".equals(mm))
239 							modifiers |= InputEvent.CTRL_MASK;
240 						else if ("ALT".equals(mm))
241 							modifiers |= InputEvent.ALT_MASK;
242 						else if ("SHIFT".equals(mm))
243 							modifiers |= InputEvent.SHIFT_MASK;
244 						else
245 							throw new IllegalStateException(mm);
246 				}
247 			}
248 			if (m.group(2).length() == 1)
249 				return KeyStroke.getKeyStroke(m.group(2).charAt(0), modifiers);
250 			final Integer kc = str2key.get(m.group(2));
251 			if (kc == null)
252 				throw new IllegalStateException(m.group(2));
253 			return KeyStroke.getKeyStroke(kc.intValue(), modifiers);
254 		} else {
255 			// swing syntax
256 			final KeyStroke k = KeyStroke.getKeyStroke(acc);
257 			if (k == null)
258 				throw new IllegalArgumentException(acc);
259 			return k;
260 		}
261 	}
262 
263 	/**
264 	 * @return never <code>null</code>
265 	 * @throws IllegalArgumentException
266 	 *             no such action found
267 	 */
268 	public Action findAction(final Class<?> controller, final String method) {
269 		try {
270 			return findAction(controller.getMethod(method, (Class<?>[]) null));
271 		} catch (final Exception e) {
272 			throw new IllegalArgumentException(controller.getName() + "::"
273 					+ method, e);
274 		}
275 	}
276 
277 	private Action findAction(final Map<Method, Action> m, final Method method) {
278 		if (m == null)
279 			throw new IllegalArgumentException(method.getDeclaringClass()
280 					.getName());
281 		final Action ret = m.get(method);
282 		if (ret == null)
283 			throw new IllegalArgumentException(method.toString());
284 		return ret;
285 	}
286 
287 	/**
288 	 * @return never <code>null</code>
289 	 * @throws IllegalArgumentException
290 	 *             no such action found
291 	 */
292 	public Action findAction(final Method method) {
293 		return findAction(c2a.get(method.getDeclaringClass()), method);
294 	}
295 
296 	/**
297 	 * @return never <code>null</code>
298 	 * @throws IllegalArgumentException
299 	 *             no such action found
300 	 */
301 	public Action findAction(final Object controller, final Method method) {
302 		return findAction(i2a.get(controller), method);
303 	}
304 
305 	/**
306 	 * @return never <code>null</code>
307 	 * @throws IllegalArgumentException
308 	 *             no such action found
309 	 */
310 	public Action findAction(final Object controller, final String method)
311 			throws IllegalArgumentException {
312 		try {
313 			return findAction(controller, controller.getClass().getMethod(
314 					method, (Class<?>[]) null));
315 		} catch (final Exception e) {
316 			throw new IllegalArgumentException(controller.getClass().getName()
317 					+ "::" + method, e);
318 		}
319 	}
320 
321 	Character findMnemonic(final CharSequence acc) {
322 		final Matcher m = MneFinder.matcher(acc);
323 		if (!m.matches())
324 			return null;
325 		return m.group(1).charAt(0);
326 	}
327 
328 	public Object registerController(final Object controller) {
329 		final Class<?> clz = controller.getClass();
330 		for (final Method me : clz.getMethods()) {
331 			final JCAction ma = me.getAnnotation(JCAction.class);
332 			if (ma == null)
333 				continue;
334 			Map<Method, Action> m = i2a.get(controller);
335 			if (m == null) {
336 				i2a.put(controller, m = new HashMap<Method, Action>());
337 				if (c2a.put(clz, m) != null)
338 					throw new IllegalStateException(clz.getName());
339 			}
340 			if (m.put(me, createAction(controller, me, ma)) != null)
341 				throw new IllegalStateException(me.toString());
342 		}
343 		return controller;
344 	}
345 
346 	/** De-escape "mnemocicced" titles. */
347 	String stripMnemonic(final CharSequence a) {
348 		return MneStripper.matcher(a).replaceAll("$1");
349 	}
350 }