/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.ui;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.IOException;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.BadLocationException;
import util.PlatformUtils;
import com.leafdigital.ui.api.*;
import leafchat.core.api.BugException;
/** Provides a basic edit box */
public class EditAreaImp extends JComponent implements FocusListener,BaseGroup
{
private JScrollPane sp;
private MyTextArea ta=new MyTextArea();
private int defaultWidth=200,defaultHeight=100;
private boolean autoStretch = false;
private int autoHeight = -1;
private int lastPreferredHeight = -1;
private String baseGroup=null;
private int topOffset=0;
EditAreaInterface externalInterface=new EditAreaInterface();
private String onChange=null,onFocus=null;
private boolean inSet=false;
@Override
public void setBounds(int x,int y,int width,int height)
{
super.setBounds(x,y,width,height);
relayout();
}
private void relayout()
{
sp.setBounds(0,topOffset,getWidth(),getHeight()-topOffset);
}
/**
* @param owner UI object
*/
public EditAreaImp(UISingleton owner)
{
setLayout(null);
sp=new JScrollPane(ta,JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
add(sp);
ta.getDocument().addDocumentListener(new DocumentListener()
{
@Override
public void insertUpdate(DocumentEvent arg0)
{
changed();
}
@Override
public void removeUpdate(DocumentEvent arg0)
{
changed();
}
@Override
public void changedUpdate(DocumentEvent arg0)
{
changed();
}
});
ta.addFocusListener(this);
}
EditArea getInterface() { return externalInterface; }
class EditAreaInterface extends BasicWidget implements EditArea,InternalWidget
{
@Override
public int getContentType() { return CONTENT_NONE; }
@Override
public void setOnChange(String sChange)
{
getInterface().getOwner().getCallbackHandler().check(sChange);
EditAreaImp.this.onChange=sChange;
}
@Override
public void setOnFocus(String sFocus)
{
getInterface().getOwner().getCallbackHandler().check(sFocus);
EditAreaImp.this.onFocus=sFocus;
}
@Override
public JComponent getJComponent()
{
return EditAreaImp.this;
}
@Override
public int getPreferredWidth()
{
return defaultWidth;
}
@Override
public int getPreferredHeight(int iWidth)
{
if(autoStretch)
{
return Math.max(defaultHeight, autoHeight) + topOffset;
}
else
{
return defaultHeight+topOffset;
}
}
@Override
public void addXMLChild(String sSlotName, Widget wChild)
{
throw new BugException("Edit boxes cannot contain children");
}
@Override
public String getValue()
{
return ta.getText();
}
@Override
public void setValue(String s)
{
try
{
inSet=true;
ta.setText(s);
updateAutoHeight();
}
finally
{
inSet=false;
}
}
@Override
public void setEnabled(boolean bEnabled)
{
ta.setEnabled(bEnabled);
}
@Override
public boolean isEnabled()
{
return ta.isEnabled();
}
@Override
public void focus()
{
getUI().focus(ta);
}
@Override
public void setWidth(int width)
{
defaultWidth=width;
}
@Override
public void setHeight(int height)
{
defaultHeight=height;
}
@Override
public void setBaseGroup(String group)
{
if(baseGroup!=null)
{
BaseGroup.Updater.removeFromGroup(EditAreaImp.this,baseGroup);
baseGroup=null;
}
if(group!=null)
{
baseGroup=group;
BaseGroup.Updater.addToGroup(EditAreaImp.this,baseGroup);
}
}
@Override
public void selectAll()
{
ta.selectAll();
}
@Override
public void highlightErrorLines(int[] lines)
{
errorLines=lines;
ta.repaint();
}
@Override
public void setAutoStretch(final boolean autoStretch)
{
UISingleton.runInSwing(new Runnable()
{
@Override
public void run()
{
if(autoStretch != EditAreaImp.this.autoStretch)
{
EditAreaImp.this.autoStretch = autoStretch;
sp.setVerticalScrollBarPolicy(
autoStretch ? JScrollPane.VERTICAL_SCROLLBAR_NEVER
: JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
}
}
});
}
}
private void changed()
{
updateAutoHeight();
if(onChange!=null && !inSet)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(onChange);
}
if(errorLines!=null)
{
errorLines=null;
repaint();
}
}
/**
* If autostretch mode is on, updates the current height.
*/
private void updateAutoHeight()
{
if(autoStretch)
{
int currentPreferredHeight = ta.getLineCount() *
ta.getFontMetrics(ta.getFont()).getHeight();
if(lastPreferredHeight == -1 || lastPreferredHeight != currentPreferredHeight)
{
lastPreferredHeight = currentPreferredHeight;
// Work out difference between scrollpane height and textarea height
int margin = sp.getInsets().top + sp.getViewport().getInsets().top +
sp.getInsets().bottom + sp.getViewport().getInsets().bottom +
sp.getHorizontalScrollBar().getPreferredSize().height;
autoHeight = currentPreferredHeight + margin;
externalInterface.redoLayout();
}
}
}
@Override
public void focusGained(FocusEvent arg0)
{
if(onFocus!=null)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(onFocus);
}
}
@Override
public void focusLost(FocusEvent arg0)
{
}
private static abstract class MyAction extends AbstractAction
{
MyAction(String name)
{
super(name);
}
abstract void update();
}
private static class CutAction extends MyAction
{
private JTextArea field;
CutAction(JTextArea field)
{
super("Cut");
this.field=field;
putValue(MNEMONIC_KEY, KeyEvent.VK_T);
putValue(ACCELERATOR_KEY,KeyStroke.getKeyStroke(KeyEvent.VK_X,
PlatformUtils.isMac()? InputEvent.META_MASK : InputEvent.CTRL_MASK));
}
@Override
void update()
{
setEnabled(field.getSelectedText()!=null);
}
@Override
public void actionPerformed(ActionEvent e)
{
field.cut();
}
}
private static class CopyAction extends MyAction
{
private JTextArea field;
CopyAction(JTextArea field)
{
super("Copy");
this.field=field;
putValue(MNEMONIC_KEY, KeyEvent.VK_C);
putValue(ACCELERATOR_KEY,KeyStroke.getKeyStroke(KeyEvent.VK_C,
PlatformUtils.isMac()? InputEvent.META_MASK : InputEvent.CTRL_MASK));
}
@Override
void update()
{
setEnabled(field.getSelectedText()!=null);
}
@Override
public void actionPerformed(ActionEvent e)
{
field.copy();
}
}
private static class PasteAction extends MyAction
{
private JTextArea field;
PasteAction(JTextArea field)
{
super("Paste");
this.field=field;
putValue(MNEMONIC_KEY, KeyEvent.VK_P);
putValue(ACCELERATOR_KEY,KeyStroke.getKeyStroke(KeyEvent.VK_V,
PlatformUtils.isMac()? InputEvent.META_MASK : InputEvent.CTRL_MASK));
}
@Override
void update()
{
Transferable t=Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
if(t==null)
setEnabled(false);
else
setEnabled(t.isDataFlavorSupported(DataFlavor.stringFlavor));
}
@Override
public void actionPerformed(ActionEvent e)
{
field.paste();
}
}
private int[] errorLines;
private class MyTextArea extends JTextArea
{
private MyAction cut,copy,paste;
private JPopupMenu pm;
MyTextArea()
{
// Setup popup menu
pm=new JPopupMenu();
cut=new CutAction(this);
copy=new CopyAction(this);
paste=new PasteAction(this);
pm.add(cut);
pm.add(copy);
pm.add(paste);
addMouseListener(new MouseAdapter()
{
@Override
public void mousePressed(MouseEvent e)
{
if(e.isPopupTrigger())
{
cut.update(); copy.update(); paste.update();
pm.show(e.getComponent(),e.getX(),e.getY());
}
}
@Override
public void mouseReleased(MouseEvent e)
{
mousePressed(e);
}
});
setBackground(new Color(0,0,0,0));
}
@Override
public void paste()
{
// Get clipboard contents
Transferable t=Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
if(t==null) return;
String text;
try
{
text=(String)t.getTransferData(DataFlavor.stringFlavor);
}
catch(UnsupportedFlavorException e)
{
e.printStackTrace();
return;
}
catch(IOException e)
{
e.printStackTrace();
return;
}
// Standardise newlines
text=text.replaceAll("\\r\\n","\n");
text=text.replaceAll("\\r","\n");
// Get rid of newlines at start and end
while(text.startsWith("\n")) text=text.substring(1);
while(text.endsWith("\n")) text=text.substring(0,text.length()-1);
// Go ahead and paste in
replaceSelection(text);
}
@Override
protected void paintComponent(Graphics g)
{
g.setColor(SystemColor.text);
g.fillRect(0,0,getWidth(),getHeight());
if(errorLines!=null)
{
g.setColor(Color.yellow);
for(int i=0;i<errorLines.length;i++)
{
int line=errorLines[i];
if(line>=getLineCount()) continue;
try
{
int pos=getLineStartOffset(line);
Rectangle r=modelToView(pos);
g.fillRect(0,r.y,getWidth(),r.height);
}
catch(BadLocationException e)
{
}
}
}
super.paintComponent(g);
}
}
@Override
public int getBaseline()
{
try
{
// TODO This probably does not work
return ta.modelToView(0).y;
}
catch(BadLocationException e)
{
return 0;
}
}
@Override
public InternalWidgetOwner getInternalWidgetOwner()
{
return (InternalWidgetOwner)externalInterface.getOwner();
}
@Override
public void setTopOffset(int topOffset)
{
if(this.topOffset==topOffset) return;
this.topOffset=topOffset;
relayout();
}
// Debugging
@Override
public void paintChildren(Graphics g)
{
super.paintChildren(g);
BaseGroup.Debug.paint(g,this,topOffset);
}
}