Welcome to the Java Programming Forums


The professional, friendly Java community. 21,500 members and growing!


The Java Programming Forums are a community of Java programmers from all around the World. Our members have a wide range of skills and they all have one thing in common: A passion to learn and code Java. We invite beginner Java programmers right through to Java professionals to post here and share your knowledge. Become a part of the community, help others, expand your knowledge of Java and enjoy talking with like minded people. Registration is quick and best of all free. We look forward to meeting you.


>> REGISTER NOW TO START POSTING


Members have full access to the forums. Advertisements are removed for registered users.

Results 1 to 7 of 7

Thread: Java Tip Jul 29, 2010 - Swing Console Component

  1. #1
    Super Moderator helloworld922's Avatar
    Join Date
    Jun 2009
    Posts
    2,896
    Thanks
    23
    Thanked 619 Times in 561 Posts
    Blog Entries
    18

    Default Java Tip Jul 29, 2010 - Swing Console Component

    Introduction

    This tutorial is focused on covering the basics of create an interactive console for Jython that you can embed into your Swing applications.

    Items that will be covered in this topic:

    1. "Piping" input/output streams using Java.
    2. Using document filters to change where text is inputted, or even prevent text from being added.
    3. Implementing a "command history" which can remember a commands that have been implemented.
    4. Using Jython from your Java program.

    Required tools

    1. All the basic tools you use for creating regular Java Swing applications.
    2. Jython. This implementation uses Jython 2.5.1, the current release at this writing. You can find the latest version here

    Difficulty: Medium-Hard. I'm assuming the reader has a thorough knowledge of the Java language and some experience with using Swing, as well as some knowledge of multi-threading in Java (very basic knowledge). There will also be exposure to some "lesser known" portions of the Java API, but I'll try to explain those when they come up. I will also explain a little bit of how to use external libraries from the Eclipse IDE.

    Streams

    To redirect the various streams, I decided to two classes, one for redirected input and the other redirected output. To simplify the design, I directly passed the JConsole object to the streams and allowed them to directly manipulate the JConsole text. Also, because data can be written/read from multiple threads, these streams have been made to be "thread safe" (well, in my mind they're thread safe, dunno if this is actually true).

    ConsoleOutputStream

    The simpler of the two, all this class needs to do is take any text inputed into it and update the text inside of the console. The methods are declared synchronized because there is a possibility that two threads could be modifying the console object at the same time. I'm just going to post the code for this class and let you read through it.

    package console.streams;
     
    import java.io.IOException;
    import java.io.Writer;
     
    import console.JConsole;
     
    /**
     * Data written to this will be displayed into the console
     * 
     * @author Andrew
     */
    public class ConsoleOutputStream extends Writer
    {
    	private JConsole	console;
     
    	/**
    	 * @param console
    	 */
    	public ConsoleOutputStream(JConsole console)
    	{
    		this.console = console;
    	}
     
    	@Override
    	public synchronized void close() throws IOException
    	{
    		console = null;
    	}
     
    	@Override
    	public void flush() throws IOException
    	{
    		// no extra flushing needed
    	}
     
    	@Override
    	public synchronized void write(char[] cbuf, int off, int len) throws IOException
    	{
    		StringBuilder temp = new StringBuilder(console.getText());
    		for (int i = off; i < off + len; i++)
    		{
    			temp.append(cbuf[i]);
    		}
    		console.setText(temp.toString());
    	}
    }

    ConsoleInputStream

    This class is similar in complexity to ConsoleOutputStream, but in addition to just reading text in, we also have to have a mechanism that allows us to add text to this stream and then read from this. For my implementation, I chose to use a StringBuilder that contains all the text that hasn't been read out yet, then as text gets read out, it's deleted from the StringBuilder. I don't think this is the most efficient method for doing this, but it gets the job done.

    There is one interesting thing to highlight:

    In order to block the read from finishing before the [enter] key has been pressed, I had to create two synchronized blocks, one to see if there is something, and the second to actually read from the block. Here's my reasoning behind this:

    The input stream is only being read from the Jython runner thread, but the text being inputted is coming from the Swing thread. If the Jython runner thread holds the lock on the input stream during the entire read, then the Swing thread will never be able to write data into the input stream. So, the read method will lock the thread to see if something can be read. If not, it releases the lock and sleeps the thread (to prevent a "live loop" which wastes cpu time). If there is, it re-acquires the lock and reads in the data.

    The class code:

    package console.streams;
     
    import java.io.IOException;
    import java.io.Reader;
     
    import console.JConsole;
     
    /**
     * Data written into this is data from the console
     * 
     * @author Andrew
     */
    public class ConsoleInputStream extends Reader
    {
    	private JConsole		console;
    	private StringBuilder	stream;
     
    	/**
    	 * @param console
    	 */
    	public ConsoleInputStream(JConsole console)
    	{
    		this.console = console;
    		stream = new StringBuilder();
    	}
     
    	/**
    	 * @param text
    	 */
    	public void addText(String text)
    	{
    		synchronized (stream)
    		{
    			stream.append(text);
    		}
    	}
     
    	@Override
    	public synchronized void close() throws IOException
    	{
    		console = null;
    		stream = null;
    	}
     
    	@Override
    	public int read(char[] buf, int off, int len) throws IOException
    	{
    		int count = 0;
    		boolean doneReading = false;
    		for (int i = off; i < off + len && !doneReading; i++)
    		{
    			// determine if we have a character we can read
    			// we need the lock for stream
    			int length = 0;
    			while (length == 0)
    			{
    				// sleep this thread until there is something to read
    				try
    				{
    					Thread.sleep(100);
    				}
    				catch (InterruptedException e)
    				{
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    				synchronized (stream)
    				{
    					length = stream.length();
    				}
    			}
    			synchronized (stream)
    			{
    				// get the character
    				buf[i] = stream.charAt(0);
    				// delete it from the buffer
    				stream.deleteCharAt(0);
    				count++;
    				if (buf[i] == '\n')
    				{
    					doneReading = true;
    				}
    			}
    		}
    		return count;
    	}
    }

    CommandHistory

    In order to keep track of previous commands the user has typed, I created a CommandHistory class which uses a circularly-linked list to keep track of the commands the user has inputted. Why a circularly-linked list? I decided that I liked being able to cycle from the oldest command back to the newest command. This is easily done by using a circularly linked list rather than having to check to see if it's the first/last item, etc.

    The CommandHistory's "top" command is the blank command. This is to allow me to easily cycle between actual history items and the empty input stream. The CommandHistory also doesn't re-add the latest item, so if the user wants to send the same command multiple times, it will allow the user to quickly cycle through all the different items without cycling up through several items just to find the next one (of course, if you have command A followed by command B, then re-run command A, it will re-add command A).

    As far as I known, this thread doesn't need to be thread-safe.

    package console;
     
    public class CommandHistory
    {
    	private class Node
    	{
    		public String	command;
    		public Node		next;
    		public Node		prev;
     
    		public Node(String command)
    		{
    			this.command = command;
    			next = null;
    			prev = null;
    		}
    	}
     
    	private int		length;
    	/**
    	 * The top command with an empty string
    	 */
    	private Node	top;
    	private Node	current;
    	private int		capacity;
     
    	/**
    	 * Creates a CommandHistory with the default capacity of 64
    	 */
    	public CommandHistory()
    	{
    		this(64);
    	}
     
    	/**
    	 * Creates a CommandHistory with a specified capacity
    	 * 
    	 * @param capacity
    	 */
    	public CommandHistory(int capacity)
    	{
    		top = new Node("");
    		current = top;
    		top.next = top;
    		top.prev = top;
    		length = 1;
    		this.capacity = capacity;
    	}
     
    	/**
    	 * @return
    	 */
    	public String getPrevCommand()
    	{
    		current = current.prev;
    		return current.command;
    	}
     
    	/**
    	 * @return
    	 */
    	public String getNextCommand()
    	{
    		current = current.next;
    		return current.command;
    	}
     
    	/**
    	 * Adds a command to this command history manager. Resets the command
    	 * counter for which command to select next/prev.<br>
    	 * If the number of remembered commands exceeds the capacity, the oldest
    	 * item is removed.<br>
    	 * Duplicate checking only for most recent item.
    	 * 
    	 * @param command
    	 */
    	public void add(String command)
    	{
    		// move back to the top
    		current = top;
    		// see if we even need to insert
    		if (top.prev.command.equals(command))
    		{
    			// don't insert
    			return;
    		}
    		// insert before top.next
    		Node temp = new Node(command);
    		Node oldPrev = top.prev;
    		temp.prev = oldPrev;
    		oldPrev.next = temp;
    		temp.next = top;
    		top.prev = temp;
    		length++;
    		if (length > capacity)
    		{
    			// delete oldest command
    			Node newNext = top.next.next;
    			top.next = newNext;
    			newNext.prev = top;
    		}
    	}
     
    	/**
    	 * @return the capacity
    	 */
    	public int getCapacity()
    	{
    		return capacity;
    	}
     
    	/**
    	 * @return the length
    	 */
    	public int getLength()
    	{
    		return length;
    	}
    }

    JConsole

    This is the main meat of the interactive console. There are several sections I want to highlight:

    The Jython engine

    All though with JSR223 Java 6 does have scripting tools, I chose not to use these because the technology is missing some features that make the console much more use-able and closer to the CPython interactive console. Instead, I chose to use an internal component of the Jython library for the Jython script engine.

    		// create streams that will link with this
    		in = new ConsoleInputStream(this);
    		// System.setIn(in);
    		out = new ConsoleOutputStream(this);
    		// System.setOut(new PrintStream(out));
    		err = new ConsoleOutputStream(this);
    		// setup the command history
    		history = new CommandHistory();
    		// setup the script engine
    		engine = new InteractiveInterpreter();
    		engine.setIn(in);
    		engine.setOut(out);
    		engine.setErr(err);

    Also, It's very important to run the Jython engine on a separate thread, otherwise the Swing thread will freeze until the Jython engine finishes executing.

    	private class PythonRunner implements Runnable
    	{
    		private String	commands;
     
    		public PythonRunner(String commands)
    		{
    			this.commands = commands;
    		}
     
    		@Override
    		public void run()
    		{
    			running = true;
    			try
    			{
    				engine.runsource(commands);
    			}
    			catch (PyException e)
    			{
    				// prints out the python error message to the console
    				e.printStackTrace();
    			}
    			// engine.eval(commands, context);
    			StringBuilder text = new StringBuilder(getText());
    			text.append(">>> ");
    			setText(text.toString());
    			running = false;
    		}
    	}

    Also, because the python engine is run on a separate thread, it is very important to properly stop the engine if it is running and the console gets disposed. Normally, I would recommend that you do not use the Thread.stop() method, but because the console is being finalized, I don't think it's too much of a problem.

    	@SuppressWarnings("deprecation")
    	@Override
    	public void finalize()
    	{
    		if (running)
    		{
    			// I know it's depracated, but since this object is being destroyed,
    			// this thread should go, too
    			pythonThread.stop();
    			pythonThread.destroy();
    		}
    	}

    Document Filters

    Document Filters allow you to easily determine what to do when the text of the document is changed. For the console, I used an index to mark what area is "editable" (i.e. the user can type/delete text from here). I also found out that the Document Filter behavior is used when the text is changed programmatically. To get around this, I used a boolean flag to determine when this filter needs to be active, and when it should be ignored.

    	private class ConsoleFilter extends DocumentFilter
    	{
    		private JConsole	console;
    		public boolean		useFilters;
     
    		public ConsoleFilter(JConsole console)
    		{
    			this.console = console;
    			useFilters = true;
    		}
     
    		@Override
    		public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr)
    				throws BadLocationException
    		{
    			if (useFilters)
    			{
    				// determine if we can insert
    				if (console.getSelectionStart() >= console.editStart)
    				{
    					// can insert
    					fb.insertString(offset, string, attr);
    				}
    				else
    				{
    					// insert at the end of the document
    					fb.insertString(console.getText().length(), string, attr);
    					// move cursor to the end
    					console.getCaret().setDot(console.getText().length());
    					// console.setSelectionEnd(console.getText().length());
    					// console.setSelectionStart(console.getText().length());
    				}
    			}
    			else
    			{
    				fb.insertString(offset, string, attr);
    			}
    		}
     
    		@Override
    		public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
    				throws BadLocationException
    		{
    			if (useFilters)
    			{
    				// determine if we can replace
    				if (console.getSelectionStart() >= console.editStart)
    				{
    					// can replace
    					fb.replace(offset, length, text, attrs);
    				}
    				else
    				{
    					// insert at end
    					fb.insertString(console.getText().length(), text, attrs);
    					// move cursor to the end
    					console.getCaret().setDot(console.getText().length());
    					// console.setSelectionEnd(console.getText().length());
    					// console.setSelectionStart(console.getText().length());
    				}
    			}
    			else
    			{
    				fb.replace(offset, length, text, attrs);
    			}
    		}
     
    		@Override
    		public void remove(DocumentFilter.FilterBypass fb, int offset, int length) throws BadLocationException
    		{
    			if (useFilters)
    			{
    				if (offset > console.editStart)
    				{
    					// can remove
    					fb.remove(offset, length);
    				}
    				else
    				{
    					// only remove the portion that's editable
    					fb.remove(console.editStart, length - (console.editStart - offset));
    					// move selection to the start of the editable section
    					console.getCaret().setDot(console.editStart);
    					// console.setSelectionStart(console.editStart);
    					// console.setSelectionEnd(console.editStart);
    				}
    			}
    			else
    			{
    				fb.remove(offset, length);
    			}
    		}
    	}

    Handling key-presses

    To provide some additional functionality, I implemented the KeyListener interface and handled certain key combinations. Once the keypress has been handled, it is important to call consume() to ensure that the base TextBox doesn't also handle these key presses.

    A few highlighted keypresses:

    [enter] - This is used to launch the Jython engine, and it is also used to put text into the input stream for the Jython engine
    [shift+enter] - Allows "multi-line" text to be passed to the Jython engine
    [ctrl+up/down] - Used for cycling through the command history

    The other keypresses were added for convenience to determine where the caret needs to move to/select, giving preference to the editable sections of the console.

    	@Override
    	public void keyPressed(KeyEvent e)
    	{
    		if (e.isControlDown())
    		{
    			if (e.getKeyCode() == KeyEvent.VK_A && !e.isShiftDown() && !e.isAltDown())
    			{
    				// handle select all
    				// if selection start is in the editable region, try to select
    				// only editable text
    				if (getSelectionStart() >= editStart)
    				{
    					// however, if we already have the editable region selected,
    					// default select all
    					if (getSelectionStart() != editStart || getSelectionEnd() != this.getText().length())
    					{
    						setSelectionStart(editStart);
    						setSelectionEnd(this.getText().length());
    						// already handled, don't use default handler
    						e.consume();
    					}
    				}
    			}
    			else if (e.getKeyCode() == KeyEvent.VK_DOWN && !e.isShiftDown() && !e.isAltDown())
    			{
    				// next in history
    				StringBuilder temp = new StringBuilder(getText());
    				// remove the current command
    				temp.delete(editStart, temp.length());
    				temp.append(history.getNextCommand());
    				setText(temp.toString(), false);
    				e.consume();
    			}
    			else if (e.getKeyCode() == KeyEvent.VK_UP && !e.isShiftDown() && !e.isAltDown())
    			{
    				// prev in history
    				StringBuilder temp = new StringBuilder(getText());
    				// remove the current command
    				temp.delete(editStart, temp.length());
    				temp.append(history.getPrevCommand());
    				setText(temp.toString(), false);
    				e.consume();
    			}
    		}
    		else if (e.getKeyCode() == KeyEvent.VK_ENTER)
    		{
    			// handle script execution
    			if (!e.isShiftDown() && !e.isAltDown())
    			{
    				if (running)
    				{
    					// we need to put text into the input stream
    					StringBuilder text = new StringBuilder(this.getText());
    					text.append(System.getProperty("line.separator"));
    					String command = text.substring(editStart);
    					setText(text.toString());
    					((ConsoleInputStream) in).addText(command);
    				}
    				else
    				{
    					// run the engine
    					StringBuilder text = new StringBuilder(this.getText());
    					String command = text.substring(editStart);
    					text.append(System.getProperty("line.separator"));
    					setText(text.toString());
    					// add to the history
    					history.add(command);
    					// run on a separate thread
    					pythonThread = new Thread(new PythonRunner(command));
    					// so this thread can't hang JVM shutdown
    					pythonThread.setDaemon(true);
    					pythonThread.start();
    				}
    				e.consume();
    			}
    			else if (!e.isAltDown())
    			{
    				// shift+enter
    				StringBuilder text = new StringBuilder(this.getText());
    				if (getSelectedText() != null)
    				{
    					// replace text
    					text.delete(getSelectionStart(), getSelectionEnd());
    				}
    				text.insert(getSelectionStart(), System.getProperty("line.separator"));
    				setText(text.toString(), false);
    			}
    		}
    		else if (e.getKeyCode() == KeyEvent.VK_HOME)
    		{
    			int selectStart = getSelectionStart();
    			if (selectStart > editStart)
    			{
    				// we're after edit start, see if we're on the same line as edit
    				// start
    				for (int i = editStart; i < selectStart; i++)
    				{
    					if (this.getText().charAt(i) == '\n')
    					{
    						// not on the same line
    						// use default handle
    						return;
    					}
    				}
    				if (e.isShiftDown())
    				{
    					// move to edit start
    					getCaret().moveDot(editStart);
    				}
    				else
    				{
    					// move select end, too
    					getCaret().setDot(editStart);
    				}
    				e.consume();
    			}
    		}
    	}

    I won't post the full code of the JConsole class here, but I will attach the source files. I'm working on getting a working Eclipse project uploaded, but currently I'm having no luck because of the file-size limit.

    Usage

    Since the JConsole extends a JTextArea, you can simply add the JConsole anywhere you would use a JTextArea.

    Here's some example code for adding the console to a JFrame:

    package console;
     
    import java.awt.GridLayout;
     
    import javax.swing.JFrame;
     
    public class JConsoleTest
    {
    	public static void main(String[] args)
    	{
    		JFrame frame = new JFrame("Jython Interactive Console");
    		frame.setSize(480, 640);
    		frame.setLayout(new GridLayout());
    		frame.add(new JConsole());
    		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    		frame.setVisible(true);
    	}
    }

    Conclusion

    I know this is a very long tutorial, but hopefully it's provided some useful information either in the individual components I used, or as a whole.

    One major thing to note about using Jython:

    Jython 2.5.1 unfortunately does NOT have a built-in help function, and many of the built-in functions do not have their doc strings set.

    License and Disclaimer:

    You are free to use this code in any way you want, it is provided "as-is" and I take no responsibility if something doesn't work correctly (please post any bugs you find, though. That would be very helpful, and I will probably update the code to fix the problem). Some credit in your final product would be nice, but I don't feel inclined to enforce/sue if you don't

    Happy Coding
    Attached Files Attached Files
    Last edited by helloworld922; August 24th, 2010 at 10:08 AM.

  2. The Following 5 Users Say Thank You to helloworld922 For This Useful Post:

    ChristopherLowe (June 28th, 2011), evotopid (June 30th, 2012), JavaPF (August 2nd, 2010), ronharry (May 20th, 2011), stu1811 (August 24th, 2010)


  3. #2
    mmm.. coffee JavaPF's Avatar
    Join Date
    May 2008
    Location
    United Kingdom
    Posts
    3,336
    My Mood
    Mellow
    Thanks
    258
    Thanked 294 Times in 227 Posts
    Blog Entries
    4

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    This is brilliant helloworld922 as usual! I'm learning from you
    Please use [highlight=Java] code [/highlight] tags when posting your code.
    Forum Tip: Add to peoples reputation by clicking the button on their useful posts.

  4. #3
    Junior Member
    Join Date
    Aug 2010
    Posts
    1
    Thanks
    0
    Thanked 0 Times in 0 Posts

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    You have no idea how long I looked for this kind of solution.
    Redirecting system.out to textarea is quite easy but the system.in was a tricky part.
    Thank you so much!!

  5. #4
    Junior Member
    Join Date
    Dec 2011
    Posts
    1
    Thanks
    0
    Thanked 0 Times in 0 Posts

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    Thanks for your help.

  6. #5
    Member
    Join Date
    Nov 2011
    Posts
    39
    Thanks
    0
    Thanked 1 Time in 1 Post

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    A great tutorial, thanks a bunch!

    Spring 3
    Last edited by cafeteria84; January 22nd, 2012 at 04:05 PM.

  7. #6
    Junior Member
    Join Date
    Jan 2012
    Location
    Mumbai
    Posts
    15
    Thanks
    0
    Thanked 0 Times in 0 Posts

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    Nice Article.. Java Rocks..

  8. #7
    Junior Member
    Join Date
    Apr 2014
    Posts
    1
    Thanks
    0
    Thanked 0 Times in 0 Posts

    Default Re: Java Tip Jul 29, 2010 - Swing Console Component

    I cant get the zipped file unzipping. I get the error, the archive is of unknown format or is damaged-WinRar

Similar Threads

  1. Java Tip Jul 5, 2010 - [Eclipse IDE] Navigating through code
    By helloworld922 in forum Java JDK & IDE Tutorials
    Replies: 1
    Last Post: July 5th, 2010, 06:28 AM
  2. How to Use a JSlider - Java Swing
    By neo_2010 in forum Java Swing Tutorials
    Replies: 4
    Last Post: March 29th, 2010, 09:33 AM
  3. Dare 2010 calls for emerging game developers
    By jeet893 in forum Java Networking
    Replies: 0
    Last Post: March 5th, 2010, 03:51 PM
  4. java swing help
    By JM_4ever in forum AWT / Java Swing
    Replies: 3
    Last Post: October 7th, 2009, 06:42 AM
  5. How to Use the JList component - Java Swing
    By neo_2010 in forum Java Swing Tutorials
    Replies: 1
    Last Post: July 11th, 2009, 04:02 AM