/*
 Subcomponent (main part) of SmileyTextArea,
   which is intended to be part of Eteria IRC Client from Javier Kohen
 2001 written by Frank Bartels for Splendid Internet GmbH, Kiel, Germany
 
 						===O=V=E=R=V=I=E=W===
 						  (over this file)
 
 					   Constants & Variables
 							Constructor
 						   Private Utils
 						   Update & Paint
 							Mouse Events
 					 Component's User Interface
 	 Class Run  (a part of a line within attributes not changing)
   Class ContentLine  (a line of runs, is one or more lines on display)
 */
 
 package com.splendid.awtchat;
 
 import java.awt.*;
 import java.awt.event.*;
 import java.awt.image.ImageObserver;
 import java.text.SimpleDateFormat;
 import java.util.*;
 import ar.com.jkohen.irc.MircMessage;
 import ar.com.jkohen.awt.NickInfoPopup;
 
 public class SmileyTextAreaArea extends Canvas implements MouseListener, MouseMotionListener, AdjustmentListener, FocusListener, ComponentListener
 {
 
   //=======================================================================
   //					   Constants & Variables
   //=======================================================================
 
   private int bufferlen = 200; // number of lines saved by default
 							  // (virtual lines, i.e. lines independent of display line length)
 
   private int dimx = 300; // reflects actual component width (initalized in constructor)
   private int dimy = 200; // reflects actual component height (initalized in constructor)
   private int borderx = 4;
   private int bordery = 3;
   private int dimxv = 294; // view area
   private int dimyv = 196;
   int subsequentindent = 12; // indent for subsequent lines (inside one append with long string)
   Vector tabs;
 
   int SMILEY_DEFAULT_SIZE = 14;
   int INTER = 6;
 
   int paint_i = 0;
   int mode = SmileyTextArea.FAST; // FAST is default
   int oldSbValue = 0;
   boolean SB_WORKAROUND_ENABLED = false;
   Scrollbar sb = null;
   int sbValue = 0;
   int sbLength = 0;
   int offset = 0;
 	
   static int WAITCOUNT = 0; // this constant is calculated on construction
   boolean inupdate = false;
   boolean inappend = false;
   boolean last_action_scrolling = false;
   boolean canBreak = true;
   boolean isSelectable = false;
 
   HyperlinkReceiver hyperlinkReceiver = null;
   CopyText copyText = null;
   Vector lines = null;
   SmileyTextArea sta = null;
   Dimension preferreddim; // reflects preferred size
 	
   // Color stuff
   static Color [] fixedColors = new Color[16]; // Init. in static block.
   private int urlColorIndex = 12;  // Standard url color
 
   private static final int bgColorIndex = 0; //16;  // background color index
   private static final int fgColorIndex = 1; //17;  // foreground color index
 
   Color backgroundColor;
   Color foregroundColor;
   Color mouseoverColor;
   Color selectedColor;
   
   // the default values are overwritten on construction
   private Font usedFonts[] = {null, null, null, null};
   private FontMetrics fmFonts[] = {null, null, null, null};
   private int fmSpaceFonts = 1;
   private int fmDescentFonts[] = {1, 1, 1, 1};
   private int fmAscentFonts[] = {1, 1, 1, 1};
   private int fmDY = 2;
   private int fmDescent = 1;
   private int fmAscent = 1;
   private int fmLeading = 0;
   
   // current settings of Graphics
   private int currentCombinedBoldItalic = 0;
   private boolean currentUnderlined = false;
   private int currentColorIndex = 0;
 
   private Vector contentLines; // instance variable for the list of ContentLines
   
   private Image img_buff;
   private Graphics gfx_buff;
   
   private NickInfoPopup nick_pop;
   private Point mouse_coords;
 	
 
   //=======================================================================
   //							Constructor
   //=======================================================================
 
 
   public void adjustmentValueChanged(AdjustmentEvent e)
   {
   	setRawSbValue(e.getValue());
   }
   
   public void focusGained(FocusEvent e)		
   {
 //	System.out.println("focusGained");
 											// if above container gets the focus...
   	last_action_scrolling = false;			// ...redisplay, because hidden areas...
 	repaint();								// ...trash the view partially.
   }
 
   public void focusLost(FocusEvent e)
   {
   }
   
   public void componentResized(ComponentEvent e)
   {
   	dimensionInit();
   }
   
   public void componentHidden(ComponentEvent e)
   {
   }
   
   public void componentMoved(ComponentEvent e)
   {
   }
   
   public void componentShown(ComponentEvent e)
   {
 	dimensionInit();
 		
   	last_action_scrolling = false;
 	repaint();
   }
 
   public SmileyTextAreaArea(SmileyTextArea sta, Scrollbar sb, HyperlinkReceiver hr, CopyText cp, NickInfoPopup np)
   {
   	super();
 	this.sta = sta;
 	this.sb = sb;
 	hyperlinkReceiver = hr;
 	copyText = cp;
 	this.nick_pop = np;
 	fmDY = 1;
 
 	SB_WORKAROUND_ENABLED = ((new Scrollbar(Scrollbar.HORIZONTAL, 20, 10, 0, 20)).getValue() == 20);
 
 	backgroundColor = fixedColors[bgColorIndex];
 	super.setBackground(backgroundColor); // for consistency and against flickering
 	foregroundColor = fixedColors[fgColorIndex];
 	super.setForeground(foregroundColor); // for consistency only
 
 //	prepareFont(getFont());  getFont()==null at this point of time
 	prepareFont(sta.DEFAULT_FONT.getName(), sta.DEFAULT_FONT.getSize());
 
 	contentLines = new Vector();
 
 	sbLength = 0;
 	sbValue = 0;
 	correctSb();
 	sb.addAdjustmentListener(this);
 
 	addFocusListener(this);				// Does not work since this does not get the keyboard focus... see focus_frame
 	addComponentListener(this);
 	dimensionInit();					// Initialize dimension by same means
 	addMouseListener(this);
 	addMouseMotionListener(this);
   }
 
   private Container focus_element = null;	// Sort of passive construction step...
   
   public void addNotify()
   {
   	super.addNotify();
 	if (focus_element == null)
 	{
 		focus_element = getContainer();
 		if (focus_element != null)
 			focus_element.addFocusListener(this);
 	}
   }
   
   public Container getContainer()
   {
   	Container c = getParent();
 	do
 	{
 		if (c != sta && c instanceof java.awt.Container)
 			return ((java.awt.Container)c);
 	  	else
 			c = c.getParent();
 			
 	} while (c != null);
 	return null;
   }
 
   //=======================================================================
   //							Private Utils
   //=======================================================================
 
   private void correctSb()
   {
   	int len = dimyv / fmDY;
 	int scrollmax = sbLength - len;
 	if (scrollmax < 0)
 		scrollmax = 0;
 	if (sbValue > scrollmax)
 		sbValue = scrollmax; // needed for dimensionInit
 	if (sbValue < 0)
 		sbValue = 0;
 	int value = scrollmax - sbValue;
 	if (value < 0)
 		value = 0;
 //	sb.setValues(value * fmDY, len * fmDY, 0, (SB_WORKAROUND_ENABLED ? scrollmax : scrollmax + len) * fmDY);
 	sb.setValues(value, len, 0, (SB_WORKAROUND_ENABLED ? scrollmax : scrollmax + len));
   }
   
   // scrolling called from outside
   
   public void setSbTop()
   {
   	setRawSbValue(0);
   }
   
   private void setRawSbValue(int value)
   {
 	// this synchonization is not very critical and it costs too much time:
 	// to be exact, it should be synchronized, because called from outside
 	
 //	offset = fmDY - (value % fmDY);
 	offset = 0;
 //	value = value / fmDY;
 	
 //	synchronized(this)
 //	{
 		int len = dimyv / fmDY;
 		int scrollmax = sbLength - len;
 		if (scrollmax < 0)
 			scrollmax = 0;
 		int sbValueOld = sbValue;
 		sbValue = scrollmax - value;
 		if (sbValue < 0)
 			sbValue = 0;
 //	}
 	if (sbValueOld != sbValue)
 	{
 		// (at end of paint())
 //		last_action_scrolling = true;
 		last_action_scrolling = false;
 		repaint();
 	}
   }
 
   private void prepareFont(String name, int size)
   {
   	usedFonts[0] = new Font(name, Font.PLAIN, size);
 	usedFonts[1] = new Font(name, Font.BOLD, size);
 	usedFonts[2] = new Font(name, Font.ITALIC, size);
 	usedFonts[3] = new Font(name, Font.BOLD | Font.ITALIC, size);
 	fmDY=0;
 	fmDescent=0;
 	fmAscent=0;
 	fmLeading=9999;
 	for (int i=0; i<4; i++)
 	{
 		FontMetrics fm=getFontMetrics(usedFonts[i]);
 		fmFonts[i]=fm;
 		fmSpaceFonts+=fm.stringWidth(" ");
 		fmDescentFonts[i]=fm.getMaxDescent();
 		fmAscentFonts[i]=fm.getMaxAscent();
 		//if (fmDY<fm.getHeight()) fmDY=fm.getHeight();				   // fmDY is max.(height)
 		if (fmDescent<fm.getMaxDescent()) fmDescent=fm.getMaxDescent(); // fmDescent is max.
 		if (fmAscent<fm.getMaxAscent()) fmAscent=fm.getMaxAscent();   // fmAscent is max.
 		if (fmLeading>fm.getLeading()) fmLeading=fm.getLeading();	 // fmLeading is min.
 	}
 	if (fmLeading<0) fmLeading=0;
 	fmDY=fmDescent+fmAscent+fmLeading + INTER; // construction of abstract fm.getHeight() for all fonts
 	fmSpaceFonts=(fmSpaceFonts+2)/4; // +2 in order to round; now fmSpaceFonts is mean value
 	if (fmDY<fmDescent+fmAscent+fmLeading) // fmDY could be too small!
 	  fmDY=fmDescent+fmAscent+fmLeading;
 	correctSb();
   }
 
   private synchronized void dimensionInit()
   {
 //  	(this) // must be synchronized, because called from outside
 //	{
 		Dimension d = getSize();
 		dimx = d.width;
 		dimy = d.height;
 		dimxv = dimx - borderx * 2;
 		dimyv = dimy - bordery * 2;
 		
 		sbLength = 0;
 		for (int i = contentLines.size() - 1; i &gt;= 0; i--)
 		{
 			ContentLine cl = (ContentLine)contentLines.elementAt(i);
 			cl.reconstruct();
 			sbLength += cl.lineDY;
 	  }
 	  if (sbValue &gt; sbLength)
 	  	sbValue = sbLength;
 	  correctSb();
 //	}
 	last_action_scrolling = false;
 	invalidate();
 	repaint();
   }
 
 /*
 	private Object[] getContentLine()
 	{
 		if (usedFonts[3] == null)
 			return(null);
   
 		p = new Point(p.x, p.y - offset);
 		int y0 = dimy - bordery + sbValue * fmDY;
    
 		if ((p.y > dimy - bordery) || (p.y < bordery))
 			return(null);
    
 		for (int i = contentLines.size() - 1; i >= 0; i--)
 		{
 			ContentLine cl = (ContentLine)contentLines.elementAt(i);
 			y0 -= cl.lineDY * fmDY;
 			if (p.y > y0)
 			{
 				Object o[] = new Object[] { cl, new Integer(y0) };
 				return(o);
 			}
 		}
 		return(null);
 	}
 */
 	private ContentLine getContentLine(Point p)
 	{
 		if (usedFonts[3] == null)
 			return(null);
   
 		p = new Point(p.x, p.y - offset);
 		int y0 = dimy - bordery + sbValue * fmDY;
    
 		if ((p.y > dimy - bordery) || (p.y < bordery))
 			return(null);
    
 		for (int i = contentLines.size() - 1; i &gt;= 0; i--)
 		{
 			ContentLine cl = (ContentLine)contentLines.elementAt(i);
 			y0 -= cl.lineDY * fmDY;
 			if (p.y &gt; y0)
 				return(cl);
 		}
 		return(null);
 	}
 	
 	private int getContentLineY(Point p)
 	{
 		if (usedFonts[3] == null)
 			return(0);
   
 		p = new Point(p.x, p.y - offset);
 		int y0 = dimy - bordery + sbValue * fmDY;
    
 		if ((p.y > dimy - bordery) || (p.y < bordery))
 			return(0);
    
 		for (int i = contentLines.size() - 1; i &gt;= 0; i--)
 		{
 			ContentLine cl = (ContentLine)contentLines.elementAt(i);
 			y0 -= cl.lineDY * fmDY;
 			if (p.y &gt; y0)
 				return(y0);
 		}
 		return(0);
 	}
 	
 	private Run getRun(Point p) // searching the run is analogous paint
 	{
 		p = new Point(p.x, p.y - offset);
  
 		ContentLine cl = getContentLine(p);
 		if (cl != null)
 		{
 			int y0 = getContentLineY(p) + fmLeading;
 		
 			// cl found, now search the run
 			int y = p.y - y0;
 			int dyline = fmAscent + fmDescent;
 	
 			for (Enumeration e = cl.runs.elements(); e.hasMoreElements(); )
 			{
 				Run run = (Run)e.nextElement();
 				int y2 = run.pixY0 + dyline + INTER;
  
 				if (run.pixY0 == run.pixY1 && y < y2 && y > run.pixY1)  // only one line, check X0 and X1
 				{
 					if (p.x > run.pixX0 && p.x < run.pixX1) return(run);
 				}
 				else if (run.pixY0 != run.pixY1 && y < y2 && y < run.pixY1)  // line broken, first line, check X0
 				{
 					if (p.x > run.pixX0) return(run);
 				}
 				else if (run.pixY0 != run.pixY1 && y > y2 && y < run.pixY1)  // line broken, between first and last line
 				{
 					if (p.x > run.pixX0) return(run);
 				}
 				else if (run.pixY0 != run.pixY1 && y > y2 && y > run.pixY1 && y < run.pixY1 + y2)  // line broken, last line, check X1
 				{
 					if (p.x > run.pixX0 && p.x < run.pixX1) return(run);
 				}
 			}
 		}
   
 		return(null);
 	}
 
   private final void deleteFirstLine()
   {
   	if (contentLines.isEmpty())
 		return;
 	sbLength -= ((ContentLine)(contentLines.firstElement())).lineDY;
 	contentLines.removeElementAt(0);
 	if (paint_i > 0)
 		paint_i--;				// undo the effect of removeElementAt for paint if paint runs
 	if (sbValue > sbLength)
 		sbValue = sbLength; 	// not really nescessary...
 	correctSb();
   }
   //=======================================================================
   //						   Update & Paint
   //=======================================================================
 
 	private void createImageBuffer(int width, int height)
 	{
 		if (width < 1)
 			width = 1;
 		if (height < 1)
 			height = 1;
 			
 		this.img_buff = createImage(width, height);
 		if (gfx_buff != null)
 			gfx_buff.dispose();
 
 		if (img_buff != null)
 			this.gfx_buff = img_buff.getGraphics();
 		else
 			this.gfx_buff = getGraphics();
 	}
 	
 	
   public void update(Graphics g)
   {
   	inupdate = true;
 	paint(g);	// everything in paint, no clear, but no need to call from here (no effect)
 	inupdate = false;
   }
 
 public synchronized void paint(Graphics g)
 {
 	if (gfx_buff == null || img_buff.getWidth(this) != dimx || img_buff.getHeight(this) != dimy)
 	{
 		createImageBuffer(dimx, dimy);
 		paintBorder(g);
 	}
 			
   	if (usedFonts[3] == null)
 	{
 		Font f = getFont();
 		if (f == null)
 			return; // must have a font...
 		prepareFont(f.getName(), f.getSize());
 	}
 
 
 	int dyLines = sbValue - oldSbValue;
 	int dy = dyLines * fmDY;
 	oldSbValue = sbValue;
 	boolean scrolling = (Math.abs(dy) < (dimy * 2) / 3);
 	
 	if (!inupdate && !inappend)
 		scrolling = false; 	// scrolling only in update() or append()
 		
 	if (!last_action_scrolling)
 		scrolling = false; 	// scrolling only if scrolling or append
 		
 	if (mode == SmileyTextArea.SAFE)
 		scrolling = false;	// scrolling is possibly not safe
 		
 	if (!isShowing())
 		scrolling = false; 	// may be overlapped by another window
 
 
 	if (scrolling && dy != 0)	
 	{
 		if (mode == SmileyTextArea.SMOOTH)
 		{
 			int step = (dy / fmDY);	// so far exact divison (rest == 0)
 			for (int yy = 0; Math.abs(yy) &lt; Math.abs(dy); yy += step)
 			{
 				if (yy != 0)
 				{
 					try
 					{
 						Thread.sleep(5);
 					} catch (InterruptedException e) {}
 				}
 
 				if (dy &lt; 0)
 					gfx_buff.copyArea(2, -step + 2 + (yy - dy + step), dimx - 4, dimy - 4 + step + (dy - step), 0, step); // up
 				else
 					gfx_buff.copyArea(2, 2 + yy, dimx - 4, dimy - 4 - step - (dy - step), 0, step); // down
 				
 				//area a littlebit too large (OK, but funny optics):
 //				if (dy < 0)
 //					g.copyArea(2, -step + 2, dimx - 4, dimy - 4 + step, 0, step); // up
 //				else
 //					g.copyArea(2, 2, dimx - 4, dimy - 4 - step, 0, step); // down
 					
 				
 				g.drawImage(img_buff, 0, 0, backgroundColor, this);
 			}
 		}
 		else
 		{
 			if (dy &lt; 0)
 				gfx_buff.copyArea(2, -dy + 2, dimx - 4, dimy - 4 + dy, 0, dy); // up
 			else
 				gfx_buff.copyArea(2, 2, dimx - 4, dimy - 4 - dy, 0, dy); // down
 				
 			g.drawImage(img_buff, 0, 0, backgroundColor, this);
 		}
 	}
 
 	if (bordery > 2)
 	{
 		gfx_buff.setColor(backgroundColor);
 		gfx_buff.fillRect(2, dimy - bordery, dimx - 4, bordery - 2);
 	}
 	
 	int lineRevY = -sbValue;
 	int maxLineRevY = (dimy - 4) / fmDY; // top
 	int minLineRevY = 0; // bottom
 
 	if (scrolling && dyLines &lt; 0)
 		maxLineRevY = -dyLines - 1;
 		
 	if (scrolling && dyLines >= 0)
 		minLineRevY = (dimy - 2 * bordery - 2 - dy) / fmDY;
 		
 
 	for (int i = 0; i &lt; contentLines.size(); i++)
 	{
 		ContentLine cl = (ContentLine)contentLines.elementAt(i);
 		cl.setMouseOver(false);
 	}
 	if (mouse_coords != null)
 	{
 		ContentLine cl = getContentLine(mouse_coords);
 		if (cl != null)
 			cl.setMouseOver(true);
 	}
 				
 	for (paint_i = contentLines.size() - 1; paint_i >= 0; paint_i--)
 	{
 	  	ContentLine cl = (ContentLine)contentLines.elementAt(paint_i);
 
 		if (lineRevY + cl.lineDY - 1 &lt; minLineRevY)
 			lineRevY += cl.lineDY; // below (skip)
 		else
 			lineRevY = cl.paint(gfx_buff, lineRevY);
 
 		if (lineRevY > maxLineRevY)
 			break; // above (we are ready)
 	}
 	
 	paint_i = 0;
 	if (!(scrolling && dyLines &lt;= 0)) // avoid clearing the top if scrolling up
 	{
 		int topdrawn = dimy - bordery - (lineRevY) * fmDY;
 		if (topdrawn > 2) // clear rest above textlines, if  there is a rest
 		{
 			gfx_buff.setColor(backgroundColor);
 			gfx_buff.fillRect(2, 2, dimx - 4, topdrawn);
 	  }
 	}
 	
 
 	if (isShowing())
 	{
 		paintRollover(gfx_buff);
 		g.drawImage(img_buff, 0, 0, backgroundColor, this);
 	}
 
 	last_action_scrolling = true;
 }
 
 	private void paintBorder(Graphics g)
 	{
 		gfx_buff.setColor(Color.gray);
 		int tempx1[] = {0, 0, dimx - 1};
 		int tempy1[] = {dimy - 1, 0, 0};
 		gfx_buff.drawPolyline(tempx1, tempy1, 3);
 		
 		gfx_buff.setColor(Color.black);
 		int tempx2[] = {1, 1, dimx - 2};
 		int tempy2[] = {dimy - 2, 1, 1};
 		gfx_buff.drawPolyline(tempx2, tempy2, 3);
 		
 		gfx_buff.setColor(Color.white);
 		int tempx3[] = {0, dimx - 1, dimx - 1};
 		int tempy3[] = {dimy - 1, dimy - 1, 0};
 		gfx_buff.drawPolyline(tempx3, tempy3, 3);
 		
 		gfx_buff.setColor(Color.lightGray);
 		int tempx4[] = {1, dimx - 2, dimx - 2};
 		int tempy4[] = {dimy - 2, dimy - 2, 1};
 		gfx_buff.drawPolyline(tempx4, tempy4, 3);
 		
 		gfx_buff.setClip(2, 2, dimx - 4, dimy - 4);
 	}
 	
 	private void paintRollover(Graphics g)
 	{
 		nick_pop.setVisible(false);
 		
 		if (mouse_coords != null)
 		{
 			String n = null;
 			ContentLine cl = getContentLine(mouse_coords);
 			if (cl != null)
 			{
 				cl.setMouseOver(true);
 				
 				n = cl.nick;
 				if (n != null)
 				{
 					nick_pop.setNick(n);
 					nick_pop.setVisible(true);
 				}
 			}
 			
 			nick_pop.setLocation(new Point(mouse_coords.x + 32, mouse_coords.y + nick_pop.getSize().height));
 		}
 
 		nick_pop.paint(g);
 	}
   
   //=======================================================================
   //							Mouse Events
   //=======================================================================
 
   public void mousePressed(MouseEvent ev)
   {
   }
   
   public void mouseReleased(MouseEvent ev)
   {
   		// This is the left click to select a line of text
 		
 		if ((ev.getModifiers() & MouseEvent.BUTTON3_MASK) == 0 && isSelectable)
 		{
 			ContentLine clicked_cl = getContentLine(ev.getPoint());
 			if (clicked_cl != null)
 				clicked_cl.setMouseOver(true);
 			
 			for (int i = 0; i < contentLines.size(); i++)
 			{
 				ContentLine cl = (ContentLine)contentLines.elementAt(i);
 				if (clicked_cl == cl)
 					cl.setSelected(!cl.isSelected());
 				else
 					cl.setSelected(false);
 			}
 			
 			last_action_scrolling = false;
 			repaint();
 		}
 
 		// This is the right click to get a cut-paste window
 				
 		if (ev.isPopupTrigger() || (ev.getModifiers() & MouseEvent.BUTTON3_MASK) != 0)
 		{
 			String s = "";
 			for (int i = 0; i < contentLines.size(); i++)
 			{
 				ContentLine cl = (ContentLine)contentLines.elementAt(i);
 				s = s + cl.text + "\n";
 			}
 			if (copyText != null)
 				copyText.addText(s);
 		}
   }
   
   public void mouseEntered(MouseEvent ev)
   {
   	mouse_coords = ev.getPoint();
 	last_action_scrolling = false;
 	repaint();
   }
   
 	public void mouseExited(MouseEvent ev)
 	{
 		mouse_coords = null;
 		last_action_scrolling = false;
 		repaint();
 	}
 
   public void mouseClicked(MouseEvent ev)
   {
   	Run run = getRun(ev.getPoint());
 	if (run != null)
 	{
 		if (!run.getUrl().equals("") && hyperlinkReceiver != null)
 			hyperlinkReceiver.handleHyperlink(run.url);
 			
 		if (!run.getNick().equals("") && hyperlinkReceiver != null)
 			hyperlinkReceiver.handleNick(run.nick);
 	}
   }
 
 	public void mouseMoved(MouseEvent ev)
 	{
 		mouse_coords = ev.getPoint();
 		last_action_scrolling = false;
 		repaint();
 
 	  	Run run = getRun(mouse_coords);
 		if (run != null)
 		{
 			if (!(run.getUrl().equals("") && run.getNick().equals("")) && hyperlinkReceiver != null)
 			{
 				setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
 				return;
 			}
 		}
 	  	setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
 	}
 
   public void mouseDragged(MouseEvent ev)
   {
   }
 
   //=======================================================================
   //						Component's User Interface
   //=======================================================================
 
 	public void append(String s, boolean interpretUrl)
 	{
 		addContent(s, interpretUrl, null);
 	}
 
 	public void append(String s, boolean interpretUrl, String n)
 	{
 		addContent(s, interpretUrl, n);
 	}
 	
 	public void clean()
 	{
 	  	contentLines.removeAllElements();
 		if (gfx_buff != null)
 		{
 			gfx_buff.dispose();
 			gfx_buff = null;
 		}
 		dimensionInit();
 	}
 	
   public void addContent(String s, boolean interpretUrl, String n)
   {
 	if (s == null || s.length() == 0)
 		s = "\n";
 	synchronized(this)
 	{
   		ContentLine cl = new ContentLine(s, interpretUrl, n);
   		cl.reconstruct();
 		contentLines.addElement(cl);
 		
 		sbLength += cl.lineDY;
 //		sbValue = 0;
 		if (oldSbValue == 0)
 			oldSbValue += cl.lineDY;
 		else
 			oldSbValue = -999;
 		correctSb();
 		
 		if (bufferlen >= 0 && contentLines.size() > bufferlen)
 			deleteFirstLine();
 	}
 	
 	if (sbValue == 0)
 	{
 		// (at end of paint())
 //		last_action_scrolling = true;
 		repaint();
 	}
 
   }
   
   public void setMode(int mode)
   {
   	synchronized(this)
 	{
 		this.mode = mode;
 		last_action_scrolling = false;
 	}
 	repaint();
   }
   
   public void setBreaks(boolean canBreak)
   {
   	this.canBreak = canBreak;
   }
 
   public void addTab(int t)
   {
   	if (tabs == null)
   		tabs = new Vector();
   	tabs.addElement(new Integer(t));
   }
   
   public void setSubsequentIndent(int indent)
   {
   	synchronized(this)
 	{
 		subsequentindent = indent;
 		last_action_scrolling = false;
 	}
 	dimensionInit();
 	// repaint(); (included in dimensionInit())
   }
   
   public int getSubsequentIndent()
   {
   	return(subsequentindent);
   }
 
   public void setBufferlen(int bufferlen)
   {
   	synchronized(this)
 	{
 		this.bufferlen = bufferlen;
 		while (bufferlen >= 0 && contentLines.size() > bufferlen)
 			deleteFirstLine();
 	}
   }
   
   public int getBufferlen()
   {
   	return(bufferlen);
   }
 
   public void setBackground(Color c)
   {
   	synchronized(this)
 	{
 		fixedColors[bgColorIndex] = c;
 		backgroundColor = c;
 		super.setBackground(backgroundColor); // for consistency and against flickering
 		last_action_scrolling = false;
 	}
 	repaint();
   }
   
   public Color getBackground()
   { return(fixedColors[bgColorIndex]);
   }
   
   public void setForeground(Color c)
   { synchronized(this)
 	{ fixedColors[fgColorIndex]=c;
 	  foregroundColor=c;
 	  super.setForeground(foregroundColor); // for consistency only
 	  last_action_scrolling=false;
 	}
 	repaint();
   }
   
   public Color getForeground()
   {
 	return(fixedColors[fgColorIndex]);
   }
   
   public void setSelectedBackground(Color c)
   {
 	mouseoverColor = c;
   }
   
   public Color getSelectedBackground()
   {
 	return(mouseoverColor);
   }
 
 	public static void setColorPalette(Color palette[])
 	{
 		fixedColors = palette;
 	}
   
   public void setColorPaletteEntry(int n, Color c)
   {
   	synchronized(this)
 	{
 		fixedColors[(n % 16)] = c;
 		last_action_scrolling = false;
 	}
 	repaint();
   }
   
   public Color getColorPaletteEntry(int n)
   {
   	return(fixedColors[(n % 16)]);
   }
 
   public void changeFont(String name, int size)
   {
   	synchronized(this)
 	{
 		prepareFont(name, size);
 		last_action_scrolling = false;
 	}
 	dimensionInit();
 	// repaint(); (included in dimensionInit())
   }
   
   public Font getFont()
   {
 	return(usedFonts[0]);
   }
 
   //=======================================================================
   //	 Class Run  (a part of a line within attributes not changing)
   //=======================================================================
 
 	// === class Run represents a text fragment including attributes ===
 	
   private class Run implements ImageObserver
   { public String text = null;
 	public String url = null;
 	public String nick = null;
 	public boolean display = true;
 	public int pixX0 = 0; // Start(0)
 	public int pixY0 = 0;
 	public int pixX1 = 0; // End(1)
 	public int pixY1 = 0;
 	public int nbreaks = 0;
 	public Vector breaks = null; // if the Run splits at lineends the text indices are saved here
 	public int combinedBoldItalic = 0;
 	public boolean underlined = false;
 	public int fgColorIndexMine = fgColorIndex;
 	public int bgColorIndexMine = bgColorIndex;
 	public Image smiley = null;
 
 	// Constructor (text)
 	
 	public Run(String atext, boolean bold, boolean italic, boolean underlined, int fgColorIndex, int bgColorIndex)
 	{
 		char c;
 		int len = atext.length();
 		text = "";
 		for (int i = 0; i < len; i++)
 		{
 			c = atext.charAt(i);
 			if (Character.isISOControl(c))
 				continue; // skip control characters
 			if (!Character.isDefined(c))
 				continue; // skip undefined characters
 			text += c;
 		}
 	  	this.combinedBoldItalic = bold ? (italic ? 3 : 1) : (italic ? 2 : 0);
 	  	this.underlined = underlined;
 	  	this.fgColorIndexMine = fgColorIndex;
 	  	this.bgColorIndexMine = bgColorIndex;
 	}
 	
 	// Constructor (smiley)
 	
 	public Run(String text, Image smiley, int bgColorIndex)
 	{
 		this.text = text;
 		this.smiley = smiley;
 		this.bgColorIndexMine = bgColorIndex;
 	}
 	
 	// if URL, you need this second construction step
 	
 	public void setUrl(String url)
 	{
 		this.url = url;
 		// preserve bold and italic and bgColorIndexMine
 		underlined = true;
 		fgColorIndexMine = urlColorIndex;
 	}
 
 	public void setNick(String nick)
 	{
 		this.nick = nick;
 	}
 	
 	public void setDisplay(boolean b)
 	{
 		this.display = b;
 	}
 	
 	public boolean display()
 	{
 		return(display);
 	}
 	
 	private int mySpaceIndex(String s, int i)
 	{
 		int len = s.length();
 	  	for (int j = i; j < len; j++)
 			if (Character.isSpaceChar(s.charAt(j)))
 				return(j);
 			
 	  	return(-1);
 	}
 
 	public int setXY(int newPixX, int newPixY, int line) // prepare the Run for display
 	{
 		String s = text;
 		int loopcount, lasti, i, x, snipped = 0;
 		
 	  	if (newPixX >= dimxv - fmSpaceFonts * 2) // break Run, needed for speed and smileys
 	  	{
 	  		newPixX = borderx + subsequentindent;
 			newPixY += fmDY;
 			line++;
 	  	}
 	  	
 	  	breaks = null;
 	  	nbreaks = 0;
 		pixX0 = newPixX;
 	  	pixY0 = newPixY;
 	  	if (smiley != null)
 	  	{
 	  		pixX1 = newPixX + smiley.getWidth(null);
 			pixY1 = newPixY;
 			if (pixX1 > dimx - borderx)
 			{
 				pixX0 = borderx + subsequentindent;
 		  		pixY0 += fmDY;
 		  		pixX1 = borderx + subsequentindent + smiley.getWidth(null);
 		  		pixY1 += fmDY;
 		  		line++;
 			}
 			return(line); // smiley case ready
 	  	}
 	  
 		pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
 		pixY1 = newPixY;
 	  
 		if (smiley != null)
 			return(line); // do not break inside a smiley
 	  	
 		loopcount = 0;
 		while (pixX1 > dimx - borderx) // then must break line
 		{
 			if (loopcount++ > 100)
 				break; // avoid deadlocks (although there are none ;-)
 			
 			for (lasti = 0, i = 0; (i = mySpaceIndex(s, i)) >= 0; )
 			{
 				x = newPixX + fmFonts[combinedBoldItalic].stringWidth(s.substring(0, i));
 				if ( x > dimx - borderx)
 					break; // found proper break (the last one on actual line)
 				i++; // pos of next char after space
 //				while (i < s.length() && Character.isSpaceChar(s.charAt(i)))
 //					i++; // hop over spaces
 		 		 lasti = i;
 			}
 			if (lasti == 0) // break complete s
 			{
 				if (newPixX <= borderx + subsequentindent) // already gone to the next line?
 				{
 					// the (spaceless) s does not fit on a line! break by force somewhere
 					for (lasti = 1, i = 1; i < s.length() - 1; i++)
 					{
 						x = newPixX + fmFonts[combinedBoldItalic].stringWidth(s.substring(0, i + 1));
 						if (x > dimx - borderx)
 						{
 							lasti = i;
 							break;
 						}
 					}
 				}
 				else
 				{
 					newPixX = borderx + subsequentindent;
 					if (pixY0 == pixY1 && breaks == null) // if break at beginning of run...
 					{
 						pixX0 = borderx + subsequentindent; // ...the beginning also must go on next line
 						pixY0 += fmDY;
 					}
 					pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
 					pixY1 += fmDY;
 					line++;
 					continue; // retry with linebreak (almost nothing to do for this) before
 				}
 			}
 		
 			if (lasti > 0 && lasti < s.length()) // break line (can not use else!)
 			{
 				if (!canBreak)
 					return(line);
 				
 				if (breaks == null)
 					breaks = new Vector();
 				breaks.addElement(new Integer(lasti + snipped));
 				nbreaks++;
 				s = s.substring(lasti);
 				snipped += lasti;
 			}
 		
 			newPixX = borderx + subsequentindent;
 			pixX1 = newPixX + fmFonts[combinedBoldItalic].stringWidth(s);
 			pixY1 += fmDY;
 			line++;
 		}
 	  
 		return(line);
 	}
 
 	private void print(Graphics g, String s, int x, int y)
 	{
 		if (smiley != null)
 		{
 		  	int dy=smiley.getHeight(this); if (dy==0) dy=SMILEY_DEFAULT_SIZE;
 			int ascent=fmAscentFonts[combinedBoldItalic];
 			int descent=fmDescentFonts[combinedBoldItalic];
 			int yoff=-(ascent-descent+dy)/2; // put in center between ascent and descent
 			if (yoff+dy-1>descent) yoff=descent-dy+1;  // but not below descent
 
 			if (bgColorIndexMine!=bgColorIndex) // Not the normal background color->paint bg first
 			{
 				int len=smiley.getWidth(this); if (len==0) len=SMILEY_DEFAULT_SIZE;
 				g.setColor(fixedColors[bgColorIndexMine]);
 	   			g.fillRect(x, y-ascent, len, ascent + descent);
 								  // +1 because rectangle goes only to n-1 on bottom
 								  // -1 because from -ascent to descent (incl) is 1 too much
 		  		g.setColor(fixedColors[fgColorIndexMine]);
 			}
 
 			g.drawImage(smiley, x, y + yoff, this);
 	  	}
 	  	else
 	  	{
 	  	  	if (bgColorIndexMine!=bgColorIndex) // Not the normal background color->paint bg first
 	  	  	{
 	  	  		int len=fmFonts[combinedBoldItalic].stringWidth(s);
 	  			int ascent=fmAscentFonts[combinedBoldItalic];
 	  			g.setColor(fixedColors[bgColorIndexMine]);
 	  			g.fillRect(x, y-ascent, len, ascent + fmDescentFonts[combinedBoldItalic]);
 								  // +1 because rectangle goes only to n-1 on bottom
 								  // -1 because from -ascent to descent (incl) is 1 too much
 				g.setColor(fixedColors[fgColorIndexMine]);
 			}
 
 			g.drawString(s, x, y);
 
 		if (underlined) // underlined->add the line
 		{
 			int len=fmFonts[combinedBoldItalic].stringWidth(s);
 			int below=fmDescentFonts[combinedBoldItalic]/4;
 		  	if (below<1) below=1; // otherwise the underline line sticks to the font
 		  	// if (getItalic())
 		  	//   would like to subtract something from len, but I don't know how much
 		  	g.drawLine(x,y+below,x+len-1,y+below); // -1 makes it comaptible with fillRect len
 		  	if (getBold())
 				g.drawLine(x,y+below+1,x+len-1,y+below+1);
 		}
 	  }
 	}
 	
 	public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h)
 	{
 		last_action_scrolling = false;
 		repaint();
 		return(true);
 	}
 	
 	public void paint(Graphics g, int y0, boolean force)
 	{
 		int x = pixX0;
 		int y = y0 + pixY0;
 		setMyGraphicsAttributes(g, force);
 		if (nbreaks == 0) // makes simple case fast
 		{
 			print(g, text, x, y);
 		}
 		else
 		{
 			int oldj = 0;
 			int j = 0;
 			for (int i = 0; i &lt;= nbreaks; i++)
 			{
 				if (i == nbreaks)
 					j = text.length();
 				else
 					j = ((Integer)(breaks.elementAt(i))).intValue();
 				print(g, text.substring(oldj, j), x, y);
 				x = borderx + subsequentindent;
 				y += fmDY;
 				oldj = j;
 			}
 	  	}
 	}
 
 	public void setMyGraphicsAttributes(Graphics g, boolean force)
 	{
 		if (currentCombinedBoldItalic != combinedBoldItalic || force)
 		{
 			g.setFont(usedFonts[combinedBoldItalic]);
 			currentCombinedBoldItalic = combinedBoldItalic;
 	  	}
 	  	if (currentColorIndex != fgColorIndexMine || force)
 	  	{
 	  		g.setColor(fixedColors[fgColorIndexMine]);
 			currentColorIndex = fgColorIndexMine;
 	  	}
 	  	// currentUnderlined != underlined does not matter for g
 	}
 
 	public String getUrl()
 	{
 		return(url == null ? "" : url);
 	}
 	
 	public String getNick()
 	{
 		return(nick == null ? "" : nick);
 	}
 	
 	public boolean getBold()
 	{
 		return((combinedBoldItalic & 1) == 1);
 	}
 	public boolean getItalic()
 	{
 		return((combinedBoldItalic & 2) == 2);
 	}
 	
 	public boolean getUnderlined()
 	{
 		return(underlined);
 	}
 	public Color getColor()
 	{
 		return(fixedColors[fgColorIndexMine]);
 	}
 	public Font getFont()
 	{
 		return(usedFonts[combinedBoldItalic]);
 	}
 	public Image getSmiley() // may return null!
 	{
 		return(smiley);
 	}
 
   } // ============ End of class Run ============
 
   //=======================================================================
   //  Class ContentLine  (a line of runs, is one or more lines on display)
   //=======================================================================
 
   private class ContentLine // === class ContentLine represents an abstract line of text ===
   { public String text;
 	public Vector runs;
 	public int lineStart;
 	public int lineEnd;
 	public int lineDY=1;
    	public String nick;
 	public boolean mouse_over;
 	public boolean line_selected;
 	public Date time_stamp;
 	
 	// Constructor (parses string and produces runs)
 
 	public ContentLine(String s, boolean interpretUrl, String n)
 	{
 		boolean boldCurrent = false;
 		boolean italicCurrent = false;
 		boolean underlineCurrent = false;
 		int bgColorIndexCurrent = bgColorIndex;
 		int fgColorIndexCurrent = fgColorIndex;
 
 		if (s.length() == 0)
 			return; // empty line, no runs
 			
 		time_stamp = new Date();
 		if (false)
 		{
 			SimpleDateFormat date_format = new SimpleDateFormat("[HH:mm:ss]");
 			s = date_format.format(time_stamp) + " " + s;
 		}
 			
 		// runs.addElement(new Run(s,false,false,false,fgColorIndex,bgColorIndex));
 		//						(simple version (without parsing))
 		text = s;
 		nick = n;
 		runs = new Vector();
 
 	  
 	  int hrefend = -1;
 	  int start = 0;
 	  int len = s.length();
 	  for (int i = 0; i &lt; len; i++)
 	  {
 	  	char ch = s.charAt(i);
 	  	
 		// find potential href's end and check for email
 /*
 		if (i > hrefend)
 		{
 			hrefend = findEndOfHref(s, i);
 			if (interpretUrl && hrefend < len && i < hrefend)
 			{
 				// is email address ?
 			
 			   if(s.charAt(hrefend) == '@')
 			   {
 			   		if (start < i)
 						runs.addElement(new Run(s.substring(start, i), boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent));
 						
    					int emailend = findEndOfHref(s, hrefend + 1);
    					String emailstr = s.substring(i, emailend);
    					Run run = new Run(emailstr, boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent);
    				
 				 	// add protocol mailto: to URL saved in run if missing so far
 				 	
 				 	if (emailstr.length() >= 7)
 				 	{
 						emailstr = "mailto:" + emailstr;
 				 	}
 				 	else
 				 	{
 				   		if (!emailstr.substring(0, 7).toUpperCase().equals("MAILTO:"))
 					 		emailstr = "mailto:" + emailstr;
 					}
 				 	run.setUrl(emailstr);
 				 	runs.addElement(run);
 				 	i = emailend - 1;
 				 	start = i + 1;
 				 	continue;
 			   }
 			}
 		}
 */
 		// check for href (including #channel)
 		if (interpretUrl && isHref(s, i))
 		{
 			if (start &lt; i)
 				runs.addElement(new Run(s.substring(start,i), boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent));
 			
 			hrefend = findEndOfHref(s, i);
 		  	String urlstr = s.substring(i, hrefend);
 			Run run = new Run(urlstr, boldCurrent, italicCurrent, underlineCurrent, fgColorIndexCurrent, bgColorIndexCurrent);
 		  
 	  		// add protocol http:// to URL saved in run if missing so far
 		  
 			if (urlstr.length() >= 4)
 				if (urlstr.substring(0, 4).toUpperCase().equals("WWW."))
 				  urlstr = "http://" + urlstr;
 				  
 //			if (urlstr.length() >= 8 && urlstr.indexOf("://") == -1)
 //				if (urlstr.indexOf("@") > 0)
 //					urlstr = "mailto:" + url