Source code: de/hunsicker/swing/util/PopupSupport.java
1 /*
2 * Copyright (c) 2001-2002, Marco Hunsicker. All rights reserved.
3 *
4 * This software is distributable under the BSD license. See the terms of the
5 * BSD license in the documentation provided with this software.
6 */
7 package de.hunsicker.swing.util;
8
9 import java.awt.AWTEvent;
10 import java.awt.Rectangle;
11 import java.awt.Toolkit;
12 import java.awt.datatransfer.DataFlavor;
13 import java.awt.datatransfer.Transferable;
14 import java.awt.event.AWTEventListener;
15 import java.awt.event.ActionEvent;
16 import java.awt.event.FocusEvent;
17 import java.awt.event.KeyAdapter;
18 import java.awt.event.KeyEvent;
19 import java.awt.event.KeyListener;
20 import java.awt.event.MouseAdapter;
21 import java.awt.event.MouseEvent;
22 import java.awt.event.MouseListener;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.List;
27
28 import javax.swing.Action;
29 import javax.swing.JComponent;
30 import javax.swing.JMenuItem;
31 import javax.swing.JPopupMenu;
32 import javax.swing.text.BadLocationException;
33 import javax.swing.text.Caret;
34 import javax.swing.text.DefaultEditorKit;
35 import javax.swing.text.Document;
36 import javax.swing.text.JTextComponent;
37 import javax.swing.text.TextAction;
38
39 import de.hunsicker.util.ResourceBundleFactory;
40
41
42 /**
43 * Helper class which adds popup menu support for {@link javax.swing.text.JTextComponent
44 * text components}.
45 *
46 * @author <a href="http://jalopy.sf.net/contact.html">Marco Hunsicker</a>
47 * @version $Revision: 1.2 $
48 */
49 public class PopupSupport
50 {
51 //~ Static variables/initializers ----------------------------------------------------
52
53 private static final Comparator COMPARATOR = new PartialStringComparator();
54 private static final String EMPTY_STRING = "" /* NOI18N */.intern();
55
56 /** The name for ResourceBundle lookup. */
57 private static final String BUNDLE_NAME =
58 "de.hunsicker.swing.util.Bundle" /* NOI18N */;
59
60 //~ Instance variables ---------------------------------------------------------------
61
62 /** The Copy action. */
63 private Action _copyAction;
64
65 /* The Cut action. */
66 private Action _cutAction;
67
68 /** The Delete action. */
69 private Action _deleteAction;
70
71 /** The Paste action. */
72 private Action _pasteAction;
73
74 /** The Select All action. */
75 private Action _selectAllAction;
76
77 /** The focus event interceptor. */
78 private FocusInterceptor _interceptor;
79
80 /** The popup menu to display. */
81 private JPopupMenu _menu;
82
83 /** Holds a list of all text components that have popup support. */
84 private List _registeredComponents; // List of <ListenerSupport>
85
86 /** List with the package names for which popup support should be enabled. */
87 private final List _supported; // List of <String>
88
89 //~ Constructors ---------------------------------------------------------------------
90
91 /**
92 * Creates a new PopupSuppport object. The popup menu support is initially enabled.
93 * The popup support will only be added for components of the
94 * <code>javax.swing</code> hierachy.
95 */
96 public PopupSupport()
97 {
98 this(true);
99 }
100
101
102 /**
103 * Creates a new PopupSuppport object. The popup menu support is initially enabled
104 * for the given hierachies or classes.
105 *
106 * @param supported supported package hierarchies or classes.
107 */
108 public PopupSupport(List supported)
109 {
110 this(true, supported);
111 }
112
113
114 /**
115 * Creates a new PopupSuppport object.
116 *
117 * @param enable if <code>true</code> the popup menu support will be initially
118 * enabled.
119 * @param supported supported package hierarchies or classes.
120 */
121 public PopupSupport(
122 boolean enable,
123 final List supported)
124 {
125 if (enable)
126 {
127 setEnabled(true);
128 }
129
130 _supported = new ArrayList(supported);
131 Collections.sort(_supported);
132 }
133
134
135 /**
136 * Creates a new PopupSuppport object.
137 *
138 * @param enable if <code>true</code> the popup menu support will be initially
139 * enabled.
140 */
141 public PopupSupport(boolean enable)
142 {
143 if (enable)
144 {
145 setEnabled(true);
146 }
147
148 _supported = new ArrayList(3);
149 _supported.add("javax.swing." /* NOI18N */);
150 }
151
152 //~ Methods --------------------------------------------------------------------------
153
154 /**
155 * Sets the status of the popup menu support.
156 *
157 * @param enable if <code>true</code> the popup menu support will be enabled.
158 */
159 public void setEnabled(boolean enable)
160 {
161 if (enable)
162 {
163 if (_interceptor == null)
164 {
165 _interceptor = new FocusInterceptor();
166 Toolkit.getDefaultToolkit().addAWTEventListener(
167 _interceptor, AWTEvent.FOCUS_EVENT_MASK);
168 }
169 }
170 else
171 {
172 Toolkit.getDefaultToolkit().removeAWTEventListener(_interceptor);
173 _interceptor = null;
174
175 // if no text component have ever got the focus no listeners were
176 // added, so we have to check
177 if (_registeredComponents != null)
178 {
179 for (int i = 0, size = _registeredComponents.size(); i < size; i++)
180 {
181 ListenerSupport support =
182 (ListenerSupport) _registeredComponents.get(i);
183 support.remove();
184 }
185 }
186
187 _registeredComponents = null;
188 _copyAction = null;
189 _cutAction = null;
190 _selectAllAction = null;
191 _pasteAction = null;
192 _deleteAction = null;
193 _menu = null;
194 }
195 }
196
197
198 /**
199 * Adds a default popup menu for the given component.
200 *
201 * @param component component to add the popup menu to.
202 */
203 public void addSupport(JTextComponent component)
204 {
205 if (component == null)
206 {
207 return;
208 }
209
210 if (_registeredComponents == null)
211 {
212 _registeredComponents = new ArrayList(10);
213 }
214
215 if (!_registeredComponents.contains(new ListenerSupport(component)))
216 {
217 ListenerSupport support =
218 new ListenerSupport(component, new MouseHandler(), new KeyHandler());
219 _registeredComponents.add(support);
220 }
221 else
222 {
223 updateSelectAllAction(component.getDocument());
224 updateDeleteAction(component);
225 }
226 }
227
228
229 /**
230 * Indicates whether the system clipboard contains text content.
231 *
232 * @return <code>true</code> if the system clipboard contains no text content.
233 */
234 protected boolean isClipboardEmpty()
235 {
236 Transferable data =
237 Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this);
238
239 if ((data == null) || !data.isDataFlavorSupported(DataFlavor.stringFlavor))
240 {
241 return true;
242 }
243
244 return false;
245 }
246
247
248 /**
249 * Returns the popup menu for the given component. The default implementation returns
250 * the same default popup menu for all components.
251 *
252 * <p>
253 * This popup menu consists of five actions:
254 * </p>
255 * <pre>
256 * +------------+
257 * | Copy |
258 * | Cut |
259 * | Paste |
260 * | Delete |
261 * +------------+
262 * | Select all |
263 * +------------+
264 * </pre>
265 *
266 * @param component component to return the popup menu for.
267 *
268 * @return the popup menu for the given component.
269 */
270 protected JPopupMenu getPopup(JTextComponent component)
271 {
272 if (_menu == null)
273 {
274 _menu = new JPopupMenu();
275
276 Action[] actions = component.getActions();
277
278 for (int i = 0; i < actions.length; i++)
279 {
280 Object value = actions[i].getValue(Action.NAME);
281
282 if (value.equals(DefaultEditorKit.cutAction))
283 {
284 _cutAction = actions[i];
285 }
286 else if (value.equals(DefaultEditorKit.copyAction))
287 {
288 _copyAction = actions[i];
289 }
290 else if (value.equals(DefaultEditorKit.pasteAction))
291 {
292 _pasteAction = actions[i];
293 }
294 else if (value.equals(DefaultEditorKit.selectAllAction))
295 {
296 _selectAllAction = actions[i];
297 }
298 }
299
300 if (_cutAction != null)
301 {
302 JMenuItem item = new JMenuItem(_cutAction);
303 item.setText(
304 ResourceBundleFactory.getBundle(BUNDLE_NAME).getString(
305 "MNU_CUT" /* NOI18N */));
306 _menu.add(item);
307 }
308
309 if (_copyAction != null)
310 {
311 JMenuItem item = new JMenuItem(_copyAction);
312 item.setText(
313 ResourceBundleFactory.getBundle(BUNDLE_NAME).getString(
314 "MNU_COPY" /* NOI18N */));
315 _menu.add(item);
316 }
317
318 if (_pasteAction != null)
319 {
320 JMenuItem item = new JMenuItem(_pasteAction);
321 item.setText(
322 ResourceBundleFactory.getBundle(BUNDLE_NAME).getString(
323 "MNU_PASTE" /* NOI18N */));
324 _menu.add(item);
325 }
326
327 if (_deleteAction == null)
328 {
329 _deleteAction = new DeleteAction();
330 }
331
332 _menu.add(_deleteAction);
333
334 if (_selectAllAction != null)
335 {
336 _menu.add(new JPopupMenu.Separator());
337
338 JMenuItem item = new JMenuItem(_selectAllAction);
339 item.setText(
340 ResourceBundleFactory.getBundle(BUNDLE_NAME).getString(
341 "MNU_SELECT_ALL" /* NOI18N */));
342 _menu.add(item);
343 }
344 }
345
346 updateCopyCutAction(component);
347 updatePasteAction(component);
348 updateDeleteAction(component);
349 updateSelectAllAction(component.getDocument());
350
351 return _menu;
352 }
353
354
355 /**
356 * Indicates whether a text selection exists.
357 *
358 * @param start start offset of the text's selection start.
359 * @param end end offset of the text's selection end.
360 *
361 * @return <code>true</code> if <code>start > end</code>.
362 */
363 protected boolean isTextSelected(
364 int start,
365 int end)
366 {
367 if (start >= end)
368 {
369 return false;
370 }
371
372 return true;
373 }
374
375
376 private void updateCopyCutAction(JTextComponent component)
377 {
378 int startOffset = component.getSelectionStart();
379 int endOffset = component.getSelectionEnd();
380
381 if (isTextSelected(startOffset, endOffset))
382 {
383 if (_copyAction != null)
384 {
385 _copyAction.setEnabled(true);
386 }
387
388 if ((_cutAction != null) && component.isEditable())
389 {
390 _cutAction.setEnabled(true);
391 }
392 }
393 else
394 {
395 if (_copyAction != null)
396 {
397 _copyAction.setEnabled(false);
398 }
399
400 if (_cutAction != null)
401 {
402 _cutAction.setEnabled(false);
403 }
404 }
405 }
406
407
408 private void updateDeleteAction(JTextComponent component)
409 {
410 if (component.isEditable() && (component.getDocument().getLength() > 0))
411 {
412 if (_deleteAction != null)
413 {
414 _deleteAction.setEnabled(true);
415 }
416 }
417 else
418 {
419 if (_deleteAction != null)
420 {
421 _deleteAction.setEnabled(false);
422 }
423 }
424 }
425
426
427 private void updatePasteAction(JTextComponent component)
428 {
429 if (_pasteAction != null)
430 {
431 if (component.isEditable() && !isClipboardEmpty())
432 {
433 _pasteAction.setEnabled(true);
434 }
435 else
436 {
437 _pasteAction.setEnabled(false);
438 }
439 }
440 }
441
442
443 /**
444 * Updates the state of the select-all text action depending on the state of the
445 * given document.
446 *
447 * @param document document of a text component.
448 */
449 private void updateSelectAllAction(Document document)
450 {
451 if (document.getLength() > 0)
452 {
453 if (_selectAllAction != null)
454 {
455 _selectAllAction.setEnabled(true);
456 }
457 }
458 else
459 {
460 if (_selectAllAction != null)
461 {
462 _selectAllAction.setEnabled(false);
463 }
464 }
465 }
466
467 //~ Inner Classes --------------------------------------------------------------------
468
469 /**
470 * Action which - depending on the selection state of a text component - either
471 * deletes the selection or the whole text.
472 */
473 private static final class DeleteAction
474 extends TextAction
475 {
476 /**
477 * Creates a new DeleteAction object.
478 */
479 public DeleteAction()
480 {
481 super("clear-action" /* NOI18N */);
482 putValue(
483 Action.NAME,
484 ResourceBundleFactory.getBundle(BUNDLE_NAME).getString(
485 "MNU_DELETE" /* NOI18N */));
486 this.setEnabled(false);
487 }
488
489 public void actionPerformed(ActionEvent ev)
490 {
491 JTextComponent target = getTextComponent(ev);
492
493 if (target == null)
494 {
495 return;
496 }
497
498 Caret caret = target.getCaret();
499 int curPos = caret.getDot();
500 int markPos = caret.getMark();
501
502 if (curPos != markPos)
503 {
504 try
505 {
506 int span = markPos - curPos;
507
508 if (span < 0)
509 {
510 span = (span * -1);
511 curPos = markPos;
512 }
513
514 Document document = target.getDocument();
515 document.remove(curPos, span);
516 }
517 catch (Exception neverOccurs)
518 {
519 ;
520 }
521 }
522 else
523 {
524 target.setText(EMPTY_STRING);
525 }
526 }
527 }
528
529
530 /**
531 * Helper class to bundle a component and associated listeners so we are able to
532 * correctly remove listenes after we're done.
533 */
534 private static final class ListenerSupport
535 {
536 JTextComponent component;
537 KeyListener keyHandler;
538 MouseListener mouseHandler;
539
540 public ListenerSupport(JTextComponent component)
541 {
542 this.component = component;
543 }
544
545
546 public ListenerSupport(
547 JTextComponent component,
548 MouseListener mouseListener,
549 KeyListener keyListener)
550 {
551 this(component);
552 this.mouseHandler = mouseListener;
553 this.keyHandler = keyListener;
554
555 add();
556 }
557
558 public void add()
559 {
560 this.component.addMouseListener(this.mouseHandler);
561 this.component.addKeyListener(this.keyHandler);
562 }
563
564
565 public boolean equals(Object o)
566 {
567 if (o instanceof JTextComponent)
568 {
569 return this.component.equals(o);
570 }
571 else if (o instanceof ListenerSupport)
572 {
573 return this.component.equals(((ListenerSupport) o).component);
574 }
575
576 return false;
577 }
578
579
580 public int hashCode()
581 {
582 return this.component.hashCode();
583 }
584
585
586 public void remove()
587 {
588 this.component.removeMouseListener(this.mouseHandler);
589 this.component.removeKeyListener(this.keyHandler);
590 }
591 }
592
593
594 private static final class PartialStringComparator
595 implements Comparator
596 {
597 public int compare(
598 Object o1,
599 Object o2)
600 {
601 String s1 = (String) o1;
602 String s2 = (String) o1;
603
604 if (s2.startsWith(s1))
605 {
606 return 0;
607 }
608
609 return s1.compareTo(s2);
610 }
611 }
612
613
614 /**
615 * Handler which 'spies' on the AWT event dispatching thread and intercepts focus
616 * events in order to add a popup menu to a text component which just gained the
617 * input focus.
618 */
619 private class FocusInterceptor
620 implements AWTEventListener
621 {
622 public void eventDispatched(AWTEvent ev)
623 {
624 if (ev.getID() == FocusEvent.FOCUS_GAINED)
625 {
626 if (ev.getSource() instanceof JTextComponent)
627 {
628 if (
629 Collections.binarySearch(
630 _supported, ev.getSource().getClass().getName(), COMPARATOR) > -1)
631 {
632 addSupport((JTextComponent) ev.getSource());
633 }
634 }
635 }
636 }
637 }
638
639
640 private final class KeyHandler
641 extends KeyAdapter
642 {
643 public void keyPressed(KeyEvent ev)
644 {
645 if (ev.isShiftDown() && (ev.getKeyCode() == KeyEvent.VK_F10))
646 {
647 JTextComponent component = (JTextComponent) ev.getSource();
648
649 if (component.isShowing())
650 {
651 try
652 {
653 Rectangle r = component.modelToView(component.getCaretPosition());
654 getPopup(component).show(component, r.x, r.y);
655 }
656 catch (BadLocationException ignored)
657 {
658 ;
659 }
660 }
661 }
662 }
663 }
664
665
666 /**
667 * Handler which updates the state of the actions for mouse events.
668 */
669 private final class MouseHandler
670 extends MouseAdapter
671 {
672 public void mousePressed(MouseEvent ev)
673 {
674 ((JComponent) ev.getSource()).requestFocus();
675 }
676
677
678 public void mouseReleased(MouseEvent ev)
679 {
680 if (ev.isPopupTrigger())
681 {
682 JTextComponent component = (JTextComponent) ev.getSource();
683 getPopup(component).show(component, ev.getX(), ev.getY());
684 }
685 }
686 }
687 }