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