1 /**
2  * Jin - a chess client for internet chess servers.
3  * More information is available at http://www.jinchess.com/.
4  * Copyright (C) 2002, 2003 Alexander Maryanovsky.
5  * All rights reserved.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
20  */
21 
22 package free.jin.console;
23 
24 import java.awt.BorderLayout;
25 import java.awt.Color;
26 import java.awt.Component;
27 import java.awt.Container;
28 import java.awt.Dimension;
29 import java.awt.LayoutManager;
30 import java.awt.Point;
31 import java.awt.Rectangle;
32 import java.awt.Toolkit;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.ContainerEvent;
36 import java.awt.event.ContainerListener;
37 import java.awt.event.FocusEvent;
38 import java.awt.event.KeyEvent;
39 import java.awt.event.KeyListener;
40 import java.awt.event.MouseEvent;
41 import java.util.Hashtable;
42 import java.util.Vector;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 import java.util.regex.PatternSyntaxException;
46 
47 import javax.swing.BorderFactory;
48 import javax.swing.BoundedRangeModel;
49 import javax.swing.JButton;
50 import javax.swing.JComponent;
51 import javax.swing.JPanel;
52 import javax.swing.JScrollBar;
53 import javax.swing.JScrollPane;
54 import javax.swing.JTextPane;
55 import javax.swing.JViewport;
56 import javax.swing.KeyStroke;
57 import javax.swing.OverlayLayout;
58 import javax.swing.SwingConstants;
59 import javax.swing.SwingUtilities;
60 import javax.swing.event.ChangeEvent;
61 import javax.swing.event.ChangeListener;
62 import javax.swing.event.EventListenerList;
63 import javax.swing.text.AttributeSet;
64 import javax.swing.text.BadLocationException;
65 import javax.swing.text.Caret;
66 import javax.swing.text.DefaultCaret;
67 import javax.swing.text.JTextComponent;
68 import javax.swing.text.Position;
69 import javax.swing.text.SimpleAttributeSet;
70 import javax.swing.text.StyleConstants;
71 import javax.swing.text.StyledDocument;
72 
73 import free.jin.I18n;
74 import free.jin.Preferences;
75 import free.util.BrowserControl;
76 import free.util.PlatformUtils;
77 
78 
79 /**
80  * A Component which implements a text console in which the user can see the
81  * output of the server and write/send arbitrary commands to the server. This
82  * is a component that can be used by various plugins - it's mainly used by
83  * free.jin.console.ConsoleManager.
84  */
85 
86 public class Console extends JPanel implements KeyListener, ContainerListener{
87 
88 
89 
90   /**
91    * The <code>ConsoleManager</code> we're a part of.
92    */
93 
94   private final ConsoleManager consoleManager;
95 
96 
97   /**
98    * The listener list.
99    */
100 
101   protected final EventListenerList listenerList = new EventListenerList();
102 
103 
104 
105   /**
106    * The ConsoleTextPane where the output is displayed.
107    */
108 
109   private final ConsoleTextPane outputComponent;
110 
111 
112 
113   /**
114    * The JScrollPane wrapping the output component.
115    */
116 
117   private final JScrollPane outputScrollPane;
118 
119 
120 
121   /**
122    * The ConsoleTextField which takes the input from the user.
123    */
124 
125   private final ConsoleTextField inputComponent;
126 
127 
128 
129   /**
130    * The preferences of this console.
131    */
132 
133   private final Preferences prefs;
134 
135 
136 
137   /**
138    * The regular expressions against which we match the text to find links.
139    */
140 
141   private Pattern [] linkREs;
142 
143 
144 
145 
146   /**
147    * The commands executed for the matched links.
148    */
149 
150   private String [] linkCommands;
151 
152 
153 
154 
155   /**
156    * The indices of the subexpression to make a link out of.
157    */
158 
159   private int [] linkSubexpressionIndices;
160 
161 
162 
163 
164   /**
165    * The regular expression we use for detecting URLs.
166    */
167 
168   private static final Pattern URL_REGEX = Pattern.compile("((([Ff][Tt][Pp]|[Hh][Tt][Tt][Pp]([Ss])?)://)|([Ww][Ww][Ww]\\.))([^\\s()<>\"])*[^\\s.,()<>\"'!?]");
169 
170 
171 
172   /**
173    * The regular expression we use for detecting emails.
174    */
175 
176   private static final Pattern EMAIL_REGEX = Pattern.compile("[^\\s()<>\"\']+@[^\\s()<>\"]+\\.[^\\s.,()<>\"'?]+");
177 
178 
179 
180 
181   /**
182    * Maps text types that were actually looked up to the resulting AttributeSets.
183    */
184 
185   private final Hashtable attributesCache = new Hashtable();
186 
187 
188 
189   /**
190    * A history of people who have told us anything.
191    */
192 
193   private final Vector tellers = new Vector();
194 
195 
196 
197   /**
198    * The amount of times addToOutput was called. See {@see #addToOutput(String, String)}
199    * for the hack involved.
200    */
201 
202   private int numAddToOutputCalls = 0;
203 
204 
205 
206   /**
207    * Whether the runnable that is supposed to scroll the scrollpane to the
208    * bottom already executed. See {@see #addToOutput(String, String)}
209    * for the hack involved.
210    */
211 
212   private boolean didScrollToBottom = true;
213 
214 
215 
216   /**
217    * Creates a new <code>Console</code> to be used in the specified <code>ConsoleManager</code>.
218    */
219 
Console(ConsoleManager consoleManager)220   public Console(ConsoleManager consoleManager){
221     this.consoleManager = consoleManager;
222     this.prefs = consoleManager.getPrefs();
223 
224     this.outputComponent = createOutputComponent();
225     configureOutputComponent(outputComponent);
226     this.outputScrollPane = createOutputScrollPane(outputComponent);
227     this.inputComponent = createInputComponent();
228 
229     registerKeyboardAction(clearingActionListener,
230         KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()),
231         WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
232 
233     createUI();
234 
235     outputComponent.addKeyListener(this);
236     inputComponent.addKeyListener(this);
237     outputComponent.addContainerListener(this);
238 
239     init();
240   }
241 
242 
243 
244   /**
245    * An action listener which clears the console.
246    */
247 
248   private final ActionListener clearingActionListener = new ActionListener(){
249     public void actionPerformed(ActionEvent evt){
250       clear();
251     }
252   };
253 
254 
255 
256   /**
257    * Creates the UI (layout) of this console.
258    */
259 
createUI()260   private void createUI(){
261     JButton clearButton = I18n.get(Console.class).createButton("clearConsoleButton");
262     clearButton.addActionListener(clearingActionListener);
263     clearButton.setRequestFocusEnabled(false);
264 
265     // We always want input component to have focus
266     inputComponent.setNextFocusableComponent(inputComponent);
267 
268     JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
269     bottomPanel.add(inputComponent, BorderLayout.CENTER);
270     bottomPanel.add(clearButton, BorderLayout.EAST);
271 
272     if (PlatformUtils.isMacOSX())
273       bottomPanel.setBorder(BorderFactory.createEmptyBorder(1, 5, 2, 18));
274     else
275       bottomPanel.setBorder(BorderFactory.createEmptyBorder(1, 5, 2, 5));
276 
277     setLayout(new BorderLayout());
278     add(outputScrollPane, BorderLayout.CENTER);
279     add(bottomPanel, BorderLayout.SOUTH);
280   }
281 
282 
283 
284   /**
285    * Returns the preferences.
286    */
287 
getPrefs()288   public Preferences getPrefs(){
289     return prefs;
290   }
291 
292 
293 
294   /**
295    * Returns the <code>ConsoleManager</code> a part of which we are.
296    */
297 
getConsoleManager()298   public ConsoleManager getConsoleManager(){
299     return consoleManager;
300   }
301 
302 
303 
304   /**
305    * Creates the <code>ConsoleTextPane</code> to which the server's textual
306    * output goes.
307    */
308 
createOutputComponent()309   protected ConsoleTextPane createOutputComponent(){
310     return new ConsoleTextPane(this);
311   }
312 
313 
314 
315   /**
316    * Configures the output component to be used with this console.
317    */
318 
configureOutputComponent(final ConsoleTextPane textPane)319   protected void configureOutputComponent(final ConsoleTextPane textPane){
320     // Seriously hack the caret for our own purposes (desired scrolling and selecting).
321     Caret caret = new DefaultCaret(){
322 
323       public void focusGained(FocusEvent evt){
324         super.focusGained(evt);
325         if (!dragging)
326           requestDefaultFocus();
327       }
328       public void focusLost(FocusEvent e){
329         this.setVisible(false);
330       }
331 
332       protected void adjustVisibility(Rectangle nloc){
333         if (!dragging)
334           return;
335 
336         if (SwingUtilities.isEventDispatchThread()){
337           textPane.scrollRectToVisible(nloc);
338           if (nloc.y+nloc.height>textPane.getSize().height-nloc.height/2){
339             BoundedRangeModel scrollModel = outputScrollPane.getVerticalScrollBar().getModel();
340             scrollModel.setValue(scrollModel.getMaximum());
341           }
342         }
343         else{
344           super.adjustVisibility(nloc); // Just in case... shouldn't happen.
345         }
346       }
347 
348       private boolean dragging = false;
349 
350       public void mousePressed(MouseEvent e){
351         dragging = true;
352         super.mousePressed(e);
353       }
354 
355       public void mouseReleased(MouseEvent e){
356         dragging = false;
357         super.mouseReleased(e);
358         if (isCopyOnSelect()){
359           SwingUtilities.invokeLater(new Runnable(){
360             public void run(){
361               requestDefaultFocus();
362             }
363           });
364         }
365       }
366 
367 
368       protected void moveCaret(MouseEvent e){
369         Point pt = new Point(e.getX(), e.getY());
370         Position.Bias[] biasRet = new Position.Bias[1];
371         int pos = textPane.getUI().viewToModel(textPane, pt, biasRet);
372         if (pos >= 0) {
373           int maxPos = textPane.getDocument().getEndPosition().getOffset();
374           if ((maxPos==pos+1)&&(pos>0)){
375             pos--;
376             moveDot(pos);
377             if (dragging){
378               BoundedRangeModel scrollModel = outputScrollPane.getVerticalScrollBar().getModel();
379               scrollModel.setValue(scrollModel.getMaximum());
380             }
381           }
382           else
383             moveDot(pos);
384         }
385       }
386 
387       protected void positionCaret(MouseEvent e) {
388         Point pt = new Point(e.getX(), e.getY());
389         Position.Bias[] biasRet = new Position.Bias[1];
390         int pos = textPane.getUI().viewToModel(textPane, pt, biasRet);
391         if (pos >= 0) {
392           int maxPos = textPane.getDocument().getEndPosition().getOffset();
393           if ((maxPos==pos+1)&&(pos>0)){
394             pos--;
395             setDot(pos);
396             if (dragging){
397               BoundedRangeModel scrollModel = outputScrollPane.getVerticalScrollBar().getModel();
398               scrollModel.setValue(scrollModel.getMaximum());
399             }
400           }
401           else
402             setDot(pos);
403         }
404       }
405 
406 
407     };
408 
409     caret.addChangeListener(new ChangeListener(){
410       public void stateChanged(ChangeEvent evt){
411         if (isCopyOnSelect())
412           textPane.copy(); // CDE/Motif style copy/paste
413       }
414     });
415 
416     textPane.setCaret(caret);
417   }
418 
419 
420 
421 
422 
423   /**
424    * The JViewport we use as the viewport for the scrollpane of the output
425    * component. This class being the viewport makes sure that when a console is
426    * resized, the currently displayed text remains such. The anchor is the last
427    * currently visible character.
428    */
429 
430   protected class OutputComponentViewport extends JViewport{
431 
432     // Used to avoid endless recursion
433     private boolean settingViewSize = false;
434 
435 
436     // This makes sure that when the viewport is resized, the last visible line
437     // (or character) remains the same after the resize.
reshape(int x, int y, int width, int height)438     public void reshape(int x, int y, int width, int height){
439       Dimension viewSize = getViewSize();
440       Dimension viewportSize = getExtentSize();
441       JTextComponent view = (JTextComponent)getView();
442 
443       if ((viewSize.height <= viewportSize.height) || (viewportSize.height < 0)
444           || settingViewSize || ((width == this.getWidth()) && (height == this.getHeight()))
445           || (view.getDocument().getLength() == 0)){
446         super.reshape(x, y, width, height);
447         return;
448       }
449 
450       Point viewPosition = getViewPosition();
451       Point viewCoords =
452         new Point(viewportSize.width + viewPosition.x, viewportSize.height + viewPosition.y);
453       int lastVisibleIndex = view.viewToModel(viewCoords);
454 
455       super.reshape(x, y, width, height);
456 
457       settingViewSize = true;
458       this.doLayout();
459       this.validate();
460       settingViewSize = false;
461       // Otherwise the viewport doesn't update what it thinks about the size of
462       // the view and may thus scroll to the wrong location.
463 
464       try{
465         Dimension newViewportSize = getExtentSize();
466         Rectangle lastVisibleIndexPosition = view.modelToView(lastVisibleIndex);
467         if (lastVisibleIndexPosition != null){
468           setViewPosition(new Point(0,
469             Math.max(0, lastVisibleIndexPosition.y + lastVisibleIndexPosition.height - 1 - newViewportSize.height)));
470         }
471       } catch (BadLocationException e){}
472     }
473 
474   }
475 
476 
477 
478 
479   /**
480    * Creates the JScrollPane in which the output component will be put.
481    */
482 
createOutputScrollPane(JTextPane outputComponent)483   protected JScrollPane createOutputScrollPane(JTextPane outputComponent){
484     JViewport viewport = new OutputComponentViewport();
485     viewport.setView(outputComponent);
486 
487     JScrollPane scrollPane = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
488     scrollPane.setViewport(viewport);
489 
490     viewport.putClientProperty("EnableWindowBlit", Boolean.TRUE);
491 
492     return scrollPane;
493   }
494 
495 
496 
497 
498   /**
499    * Creates the JTextField in which the user can input commands to be sent to
500    * the server.
501    */
502 
createInputComponent()503   protected ConsoleTextField createInputComponent(){
504     ConsoleTextField textField = new ConsoleTextField(this);
505     return textField;
506   }
507 
508 
509 
510 
511 
512   /**
513    * Assigns the default focus to input component.
514    */
515 
requestDefaultFocus()516   public boolean requestDefaultFocus(){
517     inputComponent.requestFocus();
518     return true;
519   }
520 
521 
522 
523 
524   /**
525    * Initializes this console, loading all the properties from the plugin, etc.
526    * The Console uses the Plugin's (and the User's properties) to determine its
527    * various properties (text color etc.)
528    */
529 
init()530   private void init(){
531     attributesCache.clear(); // Clear the cache
532 
533     /********************* OUTPUT COMPONENT ***********************/
534 
535     // We set it here because of a Swing bug which causes the background to be
536     // drawn with the foreground color if you set the background as an attribute.
537     Color outputBg = prefs.getColor("background", null);
538     if (outputBg != null)
539       outputComponent.setBackground(outputBg);
540 
541     Color outputSelection = prefs.getColor("output-selection", null);
542     if (outputSelection != null)
543       outputComponent.setSelectionColor(outputSelection);
544 
545     Color outputSelected = prefs.getColor("output-selected", null);
546     if (outputSelected != null)
547       outputComponent.setSelectedTextColor(outputSelected);
548 
549 
550 
551     /********************* INPUT COMPONENT *************************/
552 
553     Color inputBg = prefs.getColor("input-background", null);
554     if (inputBg != null)
555       inputComponent.setBackground(inputBg);
556 
557     Color inputFg = prefs.getColor("input-foreground", null);
558     if (inputFg != null)
559       inputComponent.setForeground(inputFg);
560 
561     Color inputSelection = prefs.getColor("input-selection", null);
562     if (inputSelection != null)
563       inputComponent.setSelectionColor(inputSelection);
564 
565     Color inputSelected = prefs.getColor("input-selected", null);
566     if (inputSelected != null)
567       inputComponent.setSelectedTextColor(inputSelected);
568 
569 
570     int numLinkPatterns = prefs.getInt("output-link.num-patterns", 0);
571     linkREs = new Pattern[numLinkPatterns];
572     linkCommands = new String[numLinkPatterns];
573     linkSubexpressionIndices = new int[numLinkPatterns];
574     for (int i = 0; i < numLinkPatterns; i++){
575       try{
576         String linkPattern = prefs.getString("output-link.pattern-" + i);
577         String linkCommand = prefs.getString("output-link.command-" + i);
578         int subexpressionIndex = prefs.getInt("output-link.index-"+i);
579 
580         linkSubexpressionIndices[i] = subexpressionIndex;
581         Pattern regex = Pattern.compile(linkPattern);
582         linkREs[i] = regex;
583         linkCommands[i] = linkCommand;
584       } catch (PatternSyntaxException e){
585           e.printStackTrace();
586         }
587     }
588   }
589 
590 
591 
592 
593   /**
594    * Refreshes the console by re-reading the plugin/user properties and
595    * adjusting the assosiated console properties accordingly. This is useful
596    * to call after a user changes the preferences.
597    */
598 
refreshFromProperties()599   public void refreshFromProperties(){
600     init();
601     outputComponent.refreshFromProperties();
602     inputComponent.refreshFromProperties();
603   }
604 
605 
606 
607 
608 
609   /**
610    * Returns whether text will be copied into the clipboard on selection.
611    */
612 
isCopyOnSelect()613   protected boolean isCopyOnSelect(){
614     return prefs.getBool("copyOnSelect", true);
615   }
616 
617 
618 
619 
620   /**
621    * This method <B>must</B> be called before adding anything to the output
622    * component. This method works together with the <code>assureScrolling</code>
623    * method.
624    *
625    * @returns whether the <code>assureScrolling</code> method should scroll the
626    * scrollpane of the output component to the bottom. This needs to be passed
627    * to the <code>assureScrolling</code> method.
628    */
629 
prepareAdding()630   protected final boolean prepareAdding(){
631     // Seriously hack the scrolling to make sure if we're at the bottom, we stay there,
632     // and if not, we stay there too :-) If you figure out what (and why) I'm doing, drop me an email,
633     // and we'll hire you as a Java programmer.
634     numAddToOutputCalls++;
635     outputScrollPane.getViewport().putClientProperty("EnableWindowBlit", Boolean.FALSE); // Adding a lot of text is slow with blitting
636     BoundedRangeModel verticalScroll = outputScrollPane.getVerticalScrollBar().getModel();
637 
638     return (verticalScroll.getMaximum()<=verticalScroll.getValue()+verticalScroll.getExtent()+5);
639     // The +5 is to scroll it to the bottom even if it's a couple of pixels away.
640     // This can happen if you try to scroll to the bottom programmatically
641     // (a bug probably) using scrollRectToVisible(Rectangle).
642   }
643 
644 
645 
646 
647   /**
648    * This method <B>must</B> be called after adding anything to the output
649    * component. This method works together with the <code>prepareAdding</code>
650    * method. Pass the value returned by <code>prepareAdding</code> as the
651    * argument of this method.
652    */
653 
assureScrolling(boolean scrollToBottom)654   protected final void assureScrolling(boolean scrollToBottom){
655     class BottomScroller implements Runnable{
656 
657       private int curNumCalls;
658 
659       BottomScroller(int curNumCalls){
660         this.curNumCalls = curNumCalls;
661       }
662 
663       public void run(){
664         if (numAddToOutputCalls == curNumCalls){
665           try{
666             int lastOffset = outputComponent.getDocument().getEndPosition().getOffset();
667             Rectangle lastCharRect = outputComponent.modelToView(lastOffset - 1);
668             outputComponent.scrollRectToVisible(lastCharRect);
669           } catch (BadLocationException e){e.printStackTrace();}
670 
671           didScrollToBottom = true;
672 
673           // Enable blitting again
674           outputScrollPane.getViewport().putClientProperty("EnableWindowBlit", Boolean.TRUE);
675 
676           outputComponent.repaint();
677         }
678         else{
679           curNumCalls = numAddToOutputCalls;
680           SwingUtilities.invokeLater(this);
681         }
682       }
683 
684     }
685 
686     if (scrollToBottom && didScrollToBottom){
687       // This may be false if the frame containing us (for example), is iconified
688       if (getPeer() != null){
689         didScrollToBottom = false;
690         SwingUtilities.invokeLater(new BottomScroller(numAddToOutputCalls));
691       }
692     }
693   }
694 
695 
696 
697 
698   /**
699    * Adds the given component to the output.
700    */
701 
addToOutput(JComponent component)702   public void addToOutput(JComponent component){
703     boolean shouldScroll = prepareAdding();
704 
705     boolean wasEditable = outputComponent.isEditable();
706     outputComponent.setEditable(true);
707     outputComponent.setCaretPosition(outputComponent.getDocument().getLength());
708     StyledDocument document = outputComponent.getStyledDocument();
709     outputComponent.insertComponent(component);
710 
711     // See http://developer.java.sun.com/developer/bugParade/bugs/4353673.html
712     LayoutManager layout = component.getParent().getLayout();
713     if (layout instanceof OverlayLayout)
714       ((OverlayLayout)layout).invalidateLayout(component.getParent());
715 
716     try{
717       document.insertString(document.getLength(), "\n", null);
718     } catch (BadLocationException e){
719         e.printStackTrace();
720       }
721     outputComponent.setEditable(wasEditable);
722 
723     assureScrolling(shouldScroll);
724   }
725 
726 
727 
728 
729   /**
730    * Adds the given text of the given text type to the output.
731    *
732    * @param text The text to add, '\n' excluded.
733    * @param textType The type of the text, "kibitz" for example.
734    */
735 
addToOutput(String text, String textType)736   public void addToOutput(String text, String textType){
737     try{
738       boolean shouldScroll = prepareAdding();
739       addToOutputImpl(text, textType);
740       assureScrolling(shouldScroll);
741     } catch (BadLocationException e){
742         e.printStackTrace(); // Why the heck is this checked?
743       }
744   }
745 
746 
747 
748   /**
749    * Actually does the work of adding the given text to the output component's
750    * Document.
751    */
752 
addToOutputImpl(String text, String textType)753   protected void addToOutputImpl(String text, String textType) throws BadLocationException{
754     StyledDocument document = outputComponent.getStyledDocument();
755     int oldTextLength = document.getLength();
756 
757     document.insertString(document.getLength(), text + "\n", attributesForTextType(textType));
758 
759     AttributeSet urlAttributes = attributesForTextType("link.url");
760     AttributeSet emailAttributes = attributesForTextType("link.email");
761     AttributeSet commandAttributes = attributesForTextType("link.command");
762 
763     Matcher urlMatcher = URL_REGEX.matcher(text);
764     while (urlMatcher.find()){
765       int matchStart = urlMatcher.start();
766       int matchEnd = urlMatcher.end();
767 
768       Command command = new Command("url "+text.substring(matchStart,matchEnd),
769         Command.SPECIAL_MASK | Command.BLANKED_MASK);
770       Position linkStart = document.createPosition(matchStart + oldTextLength);
771       Position linkEnd = document.createPosition(matchEnd + oldTextLength);
772       Link link = new Link(linkStart, linkEnd, command);
773       document.setCharacterAttributes(matchStart + oldTextLength, matchEnd - matchStart,
774         urlAttributes, false);
775       outputComponent.addLink(link);
776     }
777 
778     Matcher emailMatcher = EMAIL_REGEX.matcher(text);
779     while (emailMatcher.find()){
780       int matchStart = emailMatcher.start();
781       int matchEnd = emailMatcher.end();
782 
783       Command command = new Command("email "+text.substring(matchStart,matchEnd),
784         Command.SPECIAL_MASK | Command.BLANKED_MASK);
785       Position linkStart = document.createPosition(matchStart + oldTextLength);
786       Position linkEnd = document.createPosition(matchEnd + oldTextLength);
787       Link link = new Link(linkStart, linkEnd, command);
788       document.setCharacterAttributes(matchStart + oldTextLength, matchEnd - matchStart,
789         emailAttributes, false);
790       outputComponent.addLink(link);
791     }
792 
793     for (int i = 0; i < linkREs.length; i++){
794       Pattern linkRE = linkREs[i];
795 
796       if (linkRE == null) // Bad pattern was given in properties.
797         continue;
798 
799       Matcher linkMatcher = linkRE.matcher(text);
800       while(linkMatcher.find()){
801         String linkCommand = linkCommands[i];
802 
803         int index = -1;
804         while ((index = linkCommand.indexOf("$", index+1))!=-1){
805           if ((index<linkCommand.length()-1)&&(Character.isDigit(linkCommand.charAt(index+1)))){
806             int subexpressionIndex = Character.digit(linkCommand.charAt(index+1),10);
807             linkCommand = linkCommand.substring(0,index) + linkMatcher.group(subexpressionIndex) + linkCommand.substring(index+2);
808           }
809         }
810 
811         int linkSubexpressionIndex = linkSubexpressionIndices[i];
812         int matchStart = linkMatcher.start(linkSubexpressionIndex);
813         int matchEnd = linkMatcher.end(linkSubexpressionIndex);
814 
815         document.setCharacterAttributes(matchStart + oldTextLength, matchEnd - matchStart,
816           commandAttributes, false);
817 
818         Position linkStart = document.createPosition(matchStart + oldTextLength);
819         Position linkEnd = document.createPosition(matchEnd + oldTextLength);
820         Link link = new Link(linkStart, linkEnd, new Command(linkCommand,0));
821         outputComponent.addLink(link);
822       }
823     }
824 
825   }
826 
827 
828 
829 
830 
831   /**
832    * Returns the size of the output area.
833    */
834 
getOutputArea()835   public Dimension getOutputArea(){
836     return outputScrollPane.getViewport().getSize();
837   }
838 
839 
840 
841 
842   /**
843    * Executes a special command. The following commands are recognized by this
844    * method:
845    * <UL>
846    *   <LI> cls - Removes all text from the console.
847    *   <LI> "url <url>" - Displays the URL  (the '<' and '>' don't actually appear in the string).
848    *   <LI> "email <email address>" - Displays the mailer with the "To" field set to the given email address.
849    * </UL>
850    */
851 
executeSpecialCommand(String command)852   protected void executeSpecialCommand(String command){
853     command = command.trim();
854     if (command.equalsIgnoreCase("cls")){
855       clear();
856     }
857     else if (command.startsWith("url ")){
858       String urlString = command.substring("url ".length());
859 
860       // A www. string
861       if (urlString.substring(0, Math.min(4, urlString.length())).equalsIgnoreCase("www."))
862         urlString = "http://" + urlString; // Assume http
863 
864       if (!BrowserControl.displayURL(urlString))
865         BrowserControl.showDisplayBrowserFailedDialog(urlString, this, true);
866     }
867     else if (command.startsWith("email ")){
868       String emailString = command.substring("email ".length());
869       if (!BrowserControl.displayMailer(emailString))
870         BrowserControl.showDisplayMailerFailedDialog(emailString, this, true);
871     }
872     else{
873       String message =
874         I18n.get(Console.class).getFormattedString("unknownSpecialCommandMessage", new Object[]{command});
875       addToOutput(message, "system");
876     }
877   }
878 
879 
880 
881 
882 
883   /**
884    * Executes the given command.
885    */
886 
issueCommand(Command command)887   public void issueCommand(Command command){
888     String commandString = command.getCommandString();
889 
890     if (!command.isBlanked()){
891       addToOutput(commandString, "user");
892     }
893 
894     if (command.isSpecial())
895       executeSpecialCommand(commandString);
896     else
897       consoleManager.sendUserCommand(commandString);
898   }
899 
900 
901 
902   /**
903    * Removes all text from the console.
904    */
905 
clear()906   public void clear(){
907     outputComponent.setText("");
908     outputComponent.removeAll();
909     outputComponent.removeLinks();
910   }
911 
912 
913 
914 
915   /**
916    * Gets called when a tell by the given player is received. This method saves
917    * the name of the teller so it can be later retrieved when F9 is hit.
918    */
919 
tellReceived(String teller)920   public void tellReceived(String teller){
921     tellers.removeElement(teller);
922     tellers.insertElementAt(teller, 0);
923     if (tellers.size() > getTellerRingSize())
924       tellers.removeElementAt(tellers.size() - 1);
925   }
926 
927 
928 
929 
930   /**
931    * Returns the size of the teller ring, the amount of last players who told us
932    * something we traverse.
933    */
934 
getTellerRingSize()935   public int getTellerRingSize(){
936     return prefs.getInt("teller-ring-size", 5);
937   }
938 
939 
940 
941   /**
942    * Returns the nth (from the end) person who told us something via "tell",
943    * "say" or "atell"  which went into this console. Returns <code>null</code>
944    * if no such person exists. The index is 0 based.  Sorry about the name of
945    * the method but I didn't think getColocutor() was much better :-)
946    */
947 
getTeller(int n)948   public String getTeller(int n){
949     if ((n < 0) || (n >= tellers.size()))
950       return null;
951 
952     return (String)tellers.elementAt(n);
953   }
954 
955 
956 
957 
958   /**
959    * Returns the amount of people who have told us anything so far.
960    */
961 
getTellerCount()962   public int getTellerCount(){
963     return tellers.size();
964   }
965 
966 
967 
968   /**
969    * Returns the AttributeSet for the given type of output text. Due to a bug
970    * in Swing, this method does not address the background color.
971    */
972 
attributesForTextType(String textType)973   protected AttributeSet attributesForTextType(String textType){
974     AttributeSet attributes = (AttributeSet)attributesCache.get(textType);
975     if (attributes != null)
976       return attributes;
977 
978     String fontFamily = (String)prefs.lookup("font-family." + textType, "Monospaced");
979     Integer fontSize = (Integer)prefs.lookup("font-size." + textType, new Integer(14));
980     Boolean bold = (Boolean)prefs.lookup("font-bold." + textType, Boolean.FALSE);
981     Boolean italic = (Boolean)prefs.lookup("font-italic." + textType, Boolean.FALSE);
982     Boolean underline = (Boolean)prefs.lookup("font-underlined." + textType, Boolean.FALSE);
983     Color foreground = (Color)prefs.lookup("foreground." + textType, Color.white);
984 
985     SimpleAttributeSet mAttributes = new SimpleAttributeSet();
986     mAttributes.addAttribute(StyleConstants.FontFamily, fontFamily);
987     mAttributes.addAttribute(StyleConstants.FontSize, fontSize);
988     mAttributes.addAttribute(StyleConstants.Bold, bold);
989     mAttributes.addAttribute(StyleConstants.Italic, italic);
990     mAttributes.addAttribute(StyleConstants.Underline, underline);
991     mAttributes.addAttribute(StyleConstants.Foreground, foreground);
992 //    StyleConstants.setFontFamily(mAttributes, fontFamily);
993 //    StyleConstants.setFontSize(mAttributes, fontSize);
994 //    StyleConstants.setBold(mAttributes, bold);
995 //    StyleConstants.setItalic(mAttributes, italic);
996 //    StyleConstants.setUnderline(mAttributes, underlined);
997 //    StyleConstants.setForeground(mAttributes, foreground);
998     attributesCache.put(textType, mAttributes);
999 
1000     return mAttributes;
1001   }
1002 
1003 
1004 
1005 
1006 
1007   /**
1008    * Processes Key pressed events from the components we're registered as
1009    * listeners for. The default implementation is registered to listen to the
1010    * input component.
1011    */
1012 
keyPressed(KeyEvent evt)1013   public void keyPressed(KeyEvent evt){
1014     int keyCode = evt.getKeyCode();
1015     boolean isControlDown = evt.isControlDown();
1016 
1017     if ((evt.getSource() == inputComponent)){
1018       if (evt.getID() == KeyEvent.KEY_PRESSED){
1019         JScrollBar vscrollbar = outputScrollPane.getVerticalScrollBar();
1020         Rectangle viewRect = outputScrollPane.getViewport().getViewRect();
1021         int value = vscrollbar.getValue();
1022 
1023         switch (keyCode){
1024           case KeyEvent.VK_PAGE_UP: // Page Up
1025             vscrollbar.setValue(value -
1026               outputComponent.getScrollableBlockIncrement(viewRect,
1027               SwingConstants.VERTICAL, -1));
1028             break;
1029           case KeyEvent.VK_PAGE_DOWN: // Page Down
1030             vscrollbar.setValue(value +
1031               outputComponent.getScrollableBlockIncrement(viewRect,
1032               SwingConstants.VERTICAL, +1));
1033             break;
1034         }
1035 
1036         if (isControlDown){
1037           switch (keyCode){
1038             case KeyEvent.VK_UP: // Ctrl-Up
1039               vscrollbar.setValue(value -
1040                 outputComponent.getScrollableUnitIncrement(viewRect, SwingConstants.VERTICAL, -1));
1041               break;
1042             case KeyEvent.VK_DOWN: // Ctrl-Down
1043               vscrollbar.setValue(value +
1044                 outputComponent.getScrollableUnitIncrement(viewRect, SwingConstants.VERTICAL, +1));
1045               break;
1046             case KeyEvent.VK_HOME: // Ctrl-Home
1047               vscrollbar.setValue(vscrollbar.getMinimum());
1048               break;
1049             case KeyEvent.VK_END: // Ctrl-End
1050               vscrollbar.setValue(vscrollbar.getMaximum() - vscrollbar.getVisibleAmount());
1051               break;
1052             // case KeyEvent.VK_A: // Ctrl-A
1053             //   int documentLength = outputComponent.getDocument().getLength();
1054             //   outputComponent.setSelectionStart(0);
1055             //   outputComponent.setSelectionEnd(documentLength - 1);
1056               // The -1 here is important because otherwise it selects the end of
1057               // line at the end too, and then adding more text selects it too.
1058             //   break;
1059           }
1060         }
1061       }
1062     }
1063 
1064   }
1065 
1066 
1067 
1068 
1069   /**
1070    * Processes Key released events from the components we're registered as
1071    * listeners for. The default implementation is registered to listen to the
1072    * output and to the input component.
1073    */
1074 
keyReleased(KeyEvent evt)1075   public void keyReleased(KeyEvent evt){}
1076 
1077 
1078 
1079 
1080   /**
1081    * Processes Key typed events from the components we're registered as
1082    * listeners for. The default implementation is registered to listen to the
1083    * output and to the input component.
1084    */
1085 
keyTyped(KeyEvent evt)1086   public void keyTyped(KeyEvent evt){}
1087 
1088 
1089 
1090 
1091   /**
1092    * Listens to components being added to the output component and its descendents
1093    * and registers as the key and container listener for all of them, because
1094    * we need to transfer the focus to the input field.
1095    */
1096 
componentAdded(ContainerEvent evt)1097   public void componentAdded(ContainerEvent evt){
1098     Container container = evt.getContainer();
1099     Component child = evt.getChild();
1100 
1101     if (SwingUtilities.isDescendingFrom(container,outputComponent)) // Check just in case.
1102       registerAsListenerToHierarchy(child);
1103   }
1104 
1105 
1106 
1107 
1108   /**
1109    * Listens to components being removed from the output component and its
1110    * descendents and unregisters as the key listener.
1111    */
1112 
componentRemoved(ContainerEvent evt)1113   public void componentRemoved(ContainerEvent evt){
1114     Container container = evt.getContainer();
1115     Component child = evt.getChild();
1116 
1117     if (SwingUtilities.isDescendingFrom(container,outputComponent)) // Check just in case.
1118       unregisterAsListenerToHierarchy(child);
1119   }
1120 
1121 
1122 
1123 
1124   /**
1125    * Recursively registers <code>this</code> as the key listener with the given
1126    * component and of its descendants (recursively) if they are focus
1127    * traversable. If they are Containers, also registers as their
1128    * ContainerListener.
1129    */
1130 
registerAsListenerToHierarchy(Component component)1131   private void registerAsListenerToHierarchy(Component component){
1132     if (component.isFocusTraversable())
1133       component.addKeyListener(this);
1134 
1135     if (component instanceof Container){
1136       Container container = (Container)component;
1137       container.addContainerListener(this);
1138       int numChildren = container.getComponentCount();
1139       for (int i=0; i<numChildren; i++)
1140         registerAsListenerToHierarchy(container.getComponent(i));
1141     }
1142   }
1143 
1144 
1145 
1146 
1147   /**
1148    * Does the opposite of <code>registerAsListenerToHierarchy(Component)</code>,
1149    * unregistering <code>this</code> as the key or container listener from the
1150    * given component and any of its children.
1151    */
1152 
unregisterAsListenerToHierarchy(Component component)1153   private void unregisterAsListenerToHierarchy(Component component){
1154     if (component.isFocusTraversable())
1155       component.removeKeyListener(this);
1156 
1157     if (component instanceof Container){
1158       Container container = (Container)component;
1159       container.removeContainerListener(this);
1160       int numChildren = container.getComponentCount();
1161       for (int i=0; i<numChildren; i++)
1162         unregisterAsListenerToHierarchy(container.getComponent(i));
1163     }
1164   }
1165 
1166 
1167 }
1168