Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

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 &gt; 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 }