Source code: edu/emory/mathcs/util/swing/JConsole.java
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 *
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
8 *
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
13 *
14 * The Original Code is the Emory Utilities.
15 *
16 * The Initial Developer of the Original Code is
17 * The Distributed Computing Laboratory, Emory University.
18 * Portions created by the Initial Developer are Copyright (C) 2002
19 * the Initial Developer. All Rights Reserved.
20 *
21 * Alternatively, the contents of this file may be used under the terms of
22 * either the GNU General Public License Version 2 or later (the "GPL"), or
23 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
24 * in which case the provisions of the GPL or the LGPL are applicable instead
25 * of those above. If you wish to allow use of your version of this file only
26 * under the terms of either the GPL or the LGPL, and not to allow others to
27 * use your version of this file under the terms of the MPL, indicate your
28 * decision by deleting the provisions above and replace them with the notice
29 * and other provisions required by the GPL or the LGPL. If you do not delete
30 * the provisions above, a recipient may use your version of this file under
31 * the terms of any one of the MPL, the GPL or the LGPL.
32 *
33 * ***** END LICENSE BLOCK ***** */
34
35 package edu.emory.mathcs.util.swing;
36
37 import java.awt.Component;
38 import java.awt.Font;
39 import java.awt.Color;
40 import java.awt.Insets;
41 import java.awt.event.*;
42 import java.beans.PropertyChangeEvent;
43 import java.beans.PropertyChangeListener;
44 import java.io.*;
45 import java.util.Vector;
46 import java.awt.Cursor;
47 import javax.swing.text.*;
48 import javax.swing.*;
49 import javax.swing.JFrame;
50
51 // Things that are not in the core packages
52
53 //import bsh.util.NameCompletion;
54
55 /**
56 A JFC/Swing based console for the BeanShell desktop.
57 This is a descendant of the old AWTConsole.
58
59 Improvements by: Mark Donszelmann <Mark.Donszelmann@cern.ch>
60 including Cut & Paste
61
62 Improvements by: Daniel Leuck
63 including Color and Image support, key press bug workaround
64 */
65 public class JConsole extends JScrollPane
66 implements /*GUIConsoleInterface,*/ Runnable, KeyListener,
67 MouseListener, ActionListener, PropertyChangeListener
68 {
69 private final static String CUT = "Cut";
70 private final static String COPY = "Copy";
71 private final static String PASTE = "Paste";
72
73 private OutputStream outPipe;
74 private InputStream inPipe;
75 private InputStream in;
76 private PrintStream out;
77 private InputStream errPipe;
78 private InputStream err;
79
80 public InputStream getInputStream() { return in; }
81 public Reader getIn() { return new InputStreamReader(in); }
82 public PrintStream getOut() { return out; }
83 public PrintStream getErr() { return out; }
84
85 private int cmdStart = 0;
86 private Vector history = new Vector();
87 private String startedLine;
88 private int histLine = 0;
89
90 private boolean stopListening=false;
91
92 private JPopupMenu menu;
93 private JTextPane text;
94 private DefaultStyledDocument doc;
95
96 //NameCompletion nameCompletion;
97 final int SHOW_AMBIG_MAX = 10;
98
99 // hack to prevent key repeat for some reason?
100 private boolean gotUp = true;
101
102 private JConsole() {
103 this(null, null, null);
104 }
105
106 public JConsole( InputStream cin, OutputStream cout, InputStream err)
107 {
108 super();
109
110 // Special TextPane which catches for cut and paste, both L&F keys and
111 // programmatic behaviour
112 text = new JTextPane( doc=new DefaultStyledDocument() )
113 {
114 public void cut() {
115 if (text.getCaretPosition() < cmdStart) {
116 super.copy();
117 } else {
118 super.cut();
119 }
120 }
121
122 public void paste() {
123 forceCaretMoveToEnd();
124 super.paste();
125 }
126 };
127
128 Font font = new Font("Monospaced",Font.PLAIN,12);
129 text.setText("");
130 text.setFont( font );
131 text.setMargin( new Insets(7,5,7,5) );
132 text.addKeyListener(this);
133 setViewportView(text);
134
135 // create popup menu
136 menu = new JPopupMenu("JConsole Menu");
137 menu.add(new JMenuItem(CUT)).addActionListener(this);
138 menu.add(new JMenuItem(COPY)).addActionListener(this);
139 menu.add(new JMenuItem(PASTE)).addActionListener(this);
140
141 text.addMouseListener(this);
142
143 // make sure popup menu follows Look & Feel
144 UIManager.addPropertyChangeListener(this);
145
146 outPipe = cout;
147 if ( outPipe == null ) {
148 outPipe = new PipedOutputStream();
149 try {
150 in = new PipedInputStream((PipedOutputStream)outPipe);
151 } catch ( IOException e ) {
152 print("Console internal error (1)...", Color.red);
153 }
154 }
155
156 inPipe = cin;
157 if ( inPipe == null ) {
158 PipedOutputStream pout = new PipedOutputStream();
159 out = new PrintStream( pout );
160 try {
161 inPipe = new BlockingPipedInputStream(pout);
162 } catch ( IOException e ) { print("Console internal error: "+e); }
163 }
164 // Start the inpipe watcher
165 new Thread( this ).start();
166
167 errPipe= err;
168 if ( errPipe != null )
169 new Thread(new ErrPipeWatcher()).start();
170
171 requestFocus();
172 }
173
174 public void keyPressed( KeyEvent e ) {
175 type( e );
176 gotUp=false;
177 }
178
179 public void keyTyped(KeyEvent e) {
180 type( e );
181 }
182
183 public void keyReleased(KeyEvent e) {
184 gotUp=true;
185 type( e );
186 }
187
188 private synchronized void type( KeyEvent e ) {
189 switch ( e.getKeyCode() ) {
190
191 case ( KeyEvent.VK_ENTER ):
192 if (e.getID() == KeyEvent.KEY_PRESSED) {
193 if (gotUp) {
194 enter();
195 resetCommandStart();
196 text.setCaretPosition(cmdStart);
197 }
198 }
199 e.consume();
200 text.repaint();
201 break;
202
203 case ( KeyEvent.VK_UP ):
204 if (e.getID() == KeyEvent.KEY_PRESSED) {
205 historyUp();
206 }
207 e.consume();
208 break;
209
210 case ( KeyEvent.VK_DOWN ):
211 if (e.getID() == KeyEvent.KEY_PRESSED) {
212 historyDown();
213 }
214 e.consume();
215 break;
216
217 case ( KeyEvent.VK_LEFT ):
218 case ( KeyEvent.VK_BACK_SPACE ):
219 case ( KeyEvent.VK_DELETE ):
220 if (text.getCaretPosition() <= cmdStart) {
221 // Why isn't this working? JDK1.3 ignores this consume...
222 e.consume();
223 }
224 break;
225
226 case ( KeyEvent.VK_RIGHT ):
227 forceCaretMoveToStart();
228 break;
229
230 case ( KeyEvent.VK_HOME ):
231 text.setCaretPosition(cmdStart);
232 e.consume();
233 break;
234
235 case ( KeyEvent.VK_U ): // clear line
236 if ( (e.getModifiers() & InputEvent.CTRL_MASK) > 0 ) {
237 replaceRange( "", cmdStart, text.getText().length());
238 histLine = 0;
239 e.consume();
240 }
241 break;
242
243 case ( KeyEvent.VK_ALT ):
244 case ( KeyEvent.VK_CAPS_LOCK ):
245 case ( KeyEvent.VK_CONTROL ):
246 case ( KeyEvent.VK_META ):
247 case ( KeyEvent.VK_SHIFT ):
248 case ( KeyEvent.VK_PRINTSCREEN ):
249 case ( KeyEvent.VK_SCROLL_LOCK ):
250 case ( KeyEvent.VK_PAUSE ):
251 case ( KeyEvent.VK_INSERT ):
252 case ( KeyEvent.VK_F1):
253 case ( KeyEvent.VK_F2):
254 case ( KeyEvent.VK_F3):
255 case ( KeyEvent.VK_F4):
256 case ( KeyEvent.VK_F5):
257 case ( KeyEvent.VK_F6):
258 case ( KeyEvent.VK_F7):
259 case ( KeyEvent.VK_F8):
260 case ( KeyEvent.VK_F9):
261 case ( KeyEvent.VK_F10):
262 case ( KeyEvent.VK_F11):
263 case ( KeyEvent.VK_F12):
264 case ( KeyEvent.VK_ESCAPE ):
265
266 // only modifier pressed
267 break;
268
269 // Control-C
270 case ( KeyEvent.VK_C ):
271 if (text.getSelectedText() == null) {
272 if (( (e.getModifiers() & InputEvent.CTRL_MASK) > 0 )
273 && (e.getID() == KeyEvent.KEY_PRESSED)) {
274 append("^C");
275 }
276 e.consume();
277 }
278 break;
279
280 case ( KeyEvent.VK_TAB ):
281 // if (e.getID() == KeyEvent.KEY_RELEASED) {
282 // String part = text.getText().substring( cmdStart );
283 // doCommandCompletion( part );
284 // }
285 e.consume();
286 break;
287
288 default:
289 if (
290 (e.getModifiers() &
291 (InputEvent.CTRL_MASK
292 | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0 )
293 {
294 // plain character
295 forceCaretMoveToEnd();
296 }
297 break;
298 }
299 }
300
301 // void doCommandCompletion( String part ) {
302 // if ( nameCompletion == null )
303 // return;
304 //
305 // int i=part.length()-1;
306 //
307 // // Character.isJavaIdentifierPart() How convenient for us!!
308 // while (
309 // i >= 0 &&
310 // ( Character.isJavaIdentifierPart(part.charAt(i))
311 // || part.charAt(i) == '.' )
312 // )
313 // i--;
314 //
315 // part = part.substring(i+1);
316 //
317 // if ( part.length() < 2 ) // reasonable completion length
318 // return;
319 //
320 // //System.out.println("completing part: "+part);
321 //
322 // // no completion
323 // String [] complete = nameCompletion.completeName(part);
324 // if ( complete.length == 0 ) {
325 // java.awt.Toolkit.getDefaultToolkit().beep();
326 // return;
327 // }
328 //
329 // // Found one completion (possibly what we already have)
330 // if ( complete.length == 1 && !complete.equals(part) ) {
331 // String append = complete[0].substring(part.length());
332 // append( append );
333 // return;
334 // }
335 //
336 // // Found ambiguous, show (some of) them
337 //
338 // String line = text.getText();
339 // String command = line.substring( cmdStart );
340 // // Find prompt
341 // for(i=cmdStart; line.charAt(i) != '\n' && i > 0; i--);
342 // String prompt = line.substring( i+1, cmdStart );
343 //
344 // // Show ambiguous
345 // StringBuffer sb = new StringBuffer("\n");
346 // for( i=0; i<complete.length && i<SHOW_AMBIG_MAX; i++)
347 // sb.append( complete[i] +"\n" );
348 // if ( i == SHOW_AMBIG_MAX )
349 // sb.append("...\n");
350 //
351 // print( sb, Color.gray );
352 // print( prompt ); // print resets command start
353 // append( command ); // append does not reset command start
354 // }
355
356 private void resetCommandStart() {
357 cmdStart = text.getDocument().getLength();
358 }
359
360 private void append(String string) {
361 int slen = text.getText().length();
362 text.select(slen, slen);
363 text.replaceSelection(string);
364 }
365
366 String replaceRange(Object s, int start, int end) {
367 String st = s.toString();
368 text.select(start, end);
369 text.replaceSelection(st);
370 //text.repaint();
371 return st;
372 }
373
374 private void forceCaretMoveToEnd() {
375 if (text.getCaretPosition() < cmdStart) {
376 // move caret first!
377 text.setCaretPosition(text.getText().length());
378 }
379 text.repaint();
380 }
381
382 private void forceCaretMoveToStart() {
383 if (text.getCaretPosition() < cmdStart) {
384 // move caret first!
385 }
386 text.repaint();
387 }
388
389
390 private void enter() {
391 String s = getCmd();
392
393 if ( s.length() == 0 ) // special hack for empty return!
394 s = ";\n";
395 else {
396 history.addElement( s );
397 s = s +"\n";
398 }
399
400 append("\n");
401 histLine = 0;
402 acceptLine( s );
403 text.repaint();
404 }
405
406 private String getCmd() {
407 String s = "";
408 try {
409 s = text.getText(cmdStart, text.getText().length() - cmdStart);
410 } catch (BadLocationException e) {
411 // should not happen
412 System.out.println("Internal JConsole Error: "+e);
413 }
414 return s;
415 }
416
417 private void historyUp() {
418 if ( history.size() == 0 )
419 return;
420 if ( histLine == 0 ) // save current line
421 startedLine = getCmd();
422 if ( histLine < history.size() ) {
423 histLine++;
424 showHistoryLine();
425 }
426 }
427 private void historyDown() {
428 if ( histLine == 0 )
429 return;
430
431 histLine--;
432 showHistoryLine();
433 }
434
435 private void showHistoryLine() {
436 String showline;
437 if ( histLine == 0 )
438 showline = startedLine;
439 else
440 showline = (String)history.elementAt( history.size() - histLine );
441
442 replaceRange( showline, cmdStart, text.getText().length() );
443 text.setCaretPosition(text.getText().length());
444 text.repaint();
445 }
446
447 private void acceptLine( String line ) {
448 if (outPipe == null )
449 print("Console internal error: cannot output ...", Color.red);
450 else
451 try {
452 outPipe.write( line.getBytes() );
453 outPipe.flush();
454 } catch ( IOException e ) {
455 outPipe = null;
456 throw new RuntimeException("Console pipe broken...");
457 }
458 //text.repaint();
459 }
460
461 public void println(String string) {
462 print( string + "\n" );
463 text.repaint();
464 }
465
466 public synchronized void print(String string) {
467 append( (string==null) ? "null" : string );
468 resetCommandStart();
469 text.setCaretPosition(cmdStart);
470 }
471
472 /**
473 * Prints "\\n" (i.e. newline)
474 */
475
476 public void println() {
477 print("\n");
478 text.repaint();
479 }
480
481 public void error( String s ) {
482 print( s, Color.red );
483 }
484
485 public void println(Object object) {
486 // ugly but fast
487 print(new StringBuffer(
488 String.valueOf(object)).append("\n"));
489 text.repaint();
490 }
491
492 public void println(Icon icon) {
493 print(icon);
494 println();
495 text.repaint();
496 }
497
498 /**
499 * Prints all primitive integer values
500 * (i.e. byte, short, int, and long)
501 public void println(long l) {
502 println(String.valueOf(l));
503 }
504 */
505
506 /**
507 * Prints the primitive type "double"
508 public void println(double d) {
509 println(String.valueOf(d));
510 }
511 */
512
513 /**
514 * Prints the primitive type "float"
515 * (needed because of float->double
516 * coercion weirdness)
517 public void println(float f) {
518 println(String.valueOf(f));
519 }
520 */
521
522 /*
523 public void println(boolean b) {
524 println((b ? "true" : "false"));
525 }
526
527 public void println(char c) {
528 println(String.valueOf(c));
529 }
530 */
531
532 public synchronized void print(Object object) {
533 append(String.valueOf(object));
534 resetCommandStart();
535 text.setCaretPosition(cmdStart);
536 }
537
538 public synchronized void print(Icon icon) {
539 if (icon==null)
540 return;
541
542 text.insertIcon(icon);
543 resetCommandStart();
544 text.setCaretPosition(cmdStart);
545 }
546
547 /**
548 * Prints all primitive integer values
549 * (i.e. byte, short, int, and long)
550 public void print(long l) {
551 print(String.valueOf(l));
552 }
553 */
554
555 /**
556 * Prints the primitive type "double"
557 public void print(double d) {
558 print(String.valueOf(d));
559 }
560 */
561
562 /**
563 * Prints the primitive type "float"
564 * (needed because of float->double
565 * coercion weirdness)
566 public void print(float f) {
567 print(String.valueOf(f));
568 }
569 */
570
571 /*
572 public void print(boolean b) {
573 print(b ? "true" : "false");
574 }
575
576 public void print(char c) {
577 print(String.valueOf(c));
578 }
579 */
580
581 public void print(Object s, Font font) {
582 print(s, font, null);
583 }
584
585 public void print(Object s, Color color) {
586 print(s, null, color);
587 }
588 public void print(String s, Color color) {
589 print(s, null, color);
590 }
591
592 public synchronized void print(Object s, Font font, Color color) {
593 AttributeSet old = getStyle();
594
595 setStyle(font, color);
596 print(s);
597 setStyle(old, true);
598 }
599
600 public synchronized void print(
601 Object s,
602 String fontFamilyName,
603 int size,
604 Color color
605 ) {
606 AttributeSet old = getStyle();
607
608 setStyle(fontFamilyName, size, color);
609 print(s);
610 setStyle(old, true);
611 }
612
613 public synchronized void print(
614 Object s,
615 String fontFamilyName,
616 int size,
617 Color color,
618 boolean bold,
619 boolean italic,
620 boolean underline
621 )
622 {
623
624 AttributeSet old = getStyle();
625
626 setStyle(fontFamilyName, size, color, bold, italic, underline);
627 print(s);
628 setStyle(old, true);
629 }
630
631 public AttributeSet setStyle(Font font) {
632 return setStyle(font, null);
633 }
634
635 public AttributeSet setStyle(Color color) {
636 return setStyle(null, color);
637 }
638
639 public AttributeSet setStyle( Font font, Color color)
640 {
641 if (font!=null)
642 return setStyle( font.getFamily(), font.getSize(), color,
643 font.isBold(), font.isItalic(),
644 StyleConstants.isUnderline(getStyle()) );
645 else
646 return setStyle(null,-1,color);
647 }
648
649 public synchronized AttributeSet setStyle (
650 String fontFamilyName, int size, Color color)
651 {
652 MutableAttributeSet attr = new SimpleAttributeSet();
653 if (color!=null)
654 StyleConstants.setForeground(attr, color);
655 if (fontFamilyName!=null)
656 StyleConstants.setFontFamily(attr, fontFamilyName);
657 if (size!=-1)
658 StyleConstants.setFontSize(attr, size);
659
660 setStyle(attr);
661
662 return getStyle();
663 }
664
665 public synchronized AttributeSet setStyle(
666 String fontFamilyName,
667 int size,
668 Color color,
669 boolean bold,
670 boolean italic,
671 boolean underline
672 )
673 {
674 MutableAttributeSet attr = new SimpleAttributeSet();
675 if (color!=null)
676 StyleConstants.setForeground(attr, color);
677 if (fontFamilyName!=null)
678 StyleConstants.setFontFamily(attr, fontFamilyName);
679 if (size!=-1)
680 StyleConstants.setFontSize(attr, size);
681 StyleConstants.setBold(attr, bold);
682 StyleConstants.setItalic(attr, italic);
683 StyleConstants.setUnderline(attr, underline);
684
685 setStyle(attr);
686
687 return getStyle();
688 }
689
690 public void setStyle(AttributeSet attributes) {
691 setStyle(attributes, false);
692 }
693
694 public void setStyle(AttributeSet attributes, boolean overWrite) {
695 text.setCharacterAttributes(attributes, overWrite);
696 }
697
698 public AttributeSet getStyle() {
699 return text.getCharacterAttributes();
700 }
701
702 public void setFont( Font font ) {
703 super.setFont( font );
704
705 if ( text != null )
706 text.setFont( font );
707 }
708
709 private void inPipeWatcher() throws IOException {
710 byte [] ba = new byte [256]; // arbitrary blocking factor
711 int read; String s;
712 while ( (read = inPipe.read(ba)) != -1 && !stopListening) {
713 print( s=new String(ba, 0, read) );
714 //text.repaint();
715 }
716
717 println("Console: Input closed...");
718 }
719
720 class ErrPipeWatcher implements Runnable {
721 public void run() {
722 try {
723 byte [] ba = new byte [256]; // arbitrary blocking factor
724 int read;
725 while ( (read = errPipe.read(ba)) != -1 && !stopListening) {
726 print( new String(ba, 0, read), Color.red );
727 //text.repaint();
728 }
729
730 println("Console: Error input closed...");
731 }
732 catch (IOException e) {
733 print("Console: I/O Error: "+e+"\n", Color.red);
734 }
735 }
736 }
737
738 public void run() {
739 try {
740 inPipeWatcher();
741 } catch ( IOException e ) {
742 print("Console: I/O Error: "+e+"\n", Color.red);
743 }
744 }
745
746 public String toString() {
747 return "BeanShell console";
748 }
749
750 // MouseListener Interface
751 public void mouseClicked(MouseEvent event) {
752 }
753
754 public void mousePressed(MouseEvent event) {
755 if (event.isPopupTrigger()) {
756 menu.show(
757 (Component)event.getSource(), event.getX(), event.getY());
758 }
759 }
760
761 public void mouseReleased(MouseEvent event) {
762 if (event.isPopupTrigger()) {
763 menu.show((Component)event.getSource(), event.getX(),
764 event.getY());
765 }
766 text.repaint();
767 }
768
769 public void mouseEntered(MouseEvent event) { }
770
771 public void mouseExited(MouseEvent event) { }
772
773 // property change
774 public void propertyChange(PropertyChangeEvent event) {
775 if (event.getPropertyName().equals("lookAndFeel")) {
776 SwingUtilities.updateComponentTreeUI(menu);
777 }
778 }
779
780 // handle cut, copy and paste
781 public void actionPerformed(ActionEvent event) {
782 String cmd = event.getActionCommand();
783 if (cmd.equals(CUT)) {
784 text.cut();
785 } else if (cmd.equals(COPY)) {
786 text.copy();
787 } else if (cmd.equals(PASTE)) {
788 text.paste();
789 }
790 }
791
792 /**
793 The overridden read method in this class will not throw "Broken pipe"
794 IOExceptions; It will simply wait for new writers and data.
795 This is used by the JConsole internal read thread to allow writers
796 in different (and in particular ephemeral) threads to write to the pipe.
797
798 It also checks a little more frequently than the original read().
799
800 Warning: read() will not even error on a read to an explicitly closed
801 pipe (override closed to for that).
802 */
803 public static class BlockingPipedInputStream extends PipedInputStream
804 {
805 boolean closed;
806 public BlockingPipedInputStream( PipedOutputStream pout )
807 throws IOException
808 {
809 super(pout);
810 }
811 public synchronized int read() throws IOException {
812 if ( closed )
813 throw new IOException("stream closed");
814
815 while (super.in < 0) { // While no data */
816 notifyAll(); // Notify any writers to wake up
817 try {
818 wait(750);
819 } catch ( InterruptedException e ) {
820 throw new InterruptedIOException();
821 }
822 }
823 // This is what the superclass does.
824 int ret = buffer[super.out++] & 0xFF;
825 if (super.out >= buffer.length)
826 super.out = 0;
827 if (super.in == super.out)
828 super.in = -1; /* now empty */
829 return ret;
830 }
831 public void close() throws IOException {
832 closed = true;
833 super.close();
834 }
835 }
836
837 // public void setNameCompletion( NameCompletion nc ) {
838 // this.nameCompletion = nc;
839 // }
840
841 public void setWaitFeedback( boolean on ) {
842 if ( on )
843 setCursor( Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) );
844 else
845 setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) );
846 }
847
848 // public static void main(String [] argsv) {
849 // JFrame jfr=new JFrame();
850 // jfr.getContentPane().add(new JConsole(System.in,System.out, null));
851 // jfr.pack();
852 // jfr.setVisible(true);
853 // }
854
855 public void show(String title) {
856 show(null, title);
857 }
858
859 public void show(Component parent, String title) {
860 console.setTitle(title);
861 console.setSize(400, 300);
862 console.getContentPane().add(this);
863 if (parent != null) {
864 console.setLocationRelativeTo(parent);
865 }
866 console.addWindowListener(new java.awt.event.WindowAdapter () {
867 public void windowClosing(java.awt.event.WindowEvent e) {
868 stopListening=true;
869 }
870 }
871 );
872 console.setVisible(true);
873 }
874
875 private final JFrame console = new JFrame();
876
877 public void hide() {
878 console.setVisible(false);
879 }
880 }
881
882