/*
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 2012 Samuel Marshall.
*/
package com.leafdigital.ui;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import util.PlatformUtils;
import com.leafdigital.prefs.api.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.*;
/** Provides a basic edit box */
public class EditBoxImp extends JComponent implements ActionListener,FocusListener,BaseGroup,ThemeListener
{
private MyTextField tf=new MyTextField();
private final static int MAXHISTORY=64;
private int defaultWidth=200;
private String baseGroup=null;
private int topOffset=0;
private int lineMax=-1;
private boolean lineWrap=false;
private int[] lineBreaks=null;
private boolean useFontSettings;
EditBoxInterface ebi=new EditBoxInterface();
private String onEnter=null,onChange=null,onFocus=null,onMultiLine=null;
private TextViewImp tvi=null;
private boolean inSet=false;
private String require=null;
private static Color defaultFG=null,defaultBG=null;
/** Preference group for remembering position, if used */
private PreferencesGroup prefsGroup = null;
// TODO Is there a better way to get this colour? Shared TableImp, EditBoxImp
private static Color errorBG=new Color(255,200,200);
LinkedList<String> history = new LinkedList<String>();
int historyPos=-1;
private EditBox.TabCompletion tc=null;
private class MyDocument extends PlainDocument
{
@Override
public void insertString(int iPos,String s,AttributeSet as) throws BadLocationException
{
try
{
// Slightly complicated logic to calculate length in bytes (fun!)
int iExistingBytes=getText(0,getLength()).getBytes("UTF-8").length;
int iNewBytes=s.getBytes("UTF-8").length;
while(lineMax!=EditBox.LINEBYTES_NOLIMIT &&
iExistingBytes+iNewBytes > lineMax && !lineWrap)
{
s=s.substring(0,s.length()-1);
iNewBytes=s.getBytes("UTF-8").length;
}
}
catch(UnsupportedEncodingException e)
{
assert false;
}
super.insertString(iPos,s,as);
}
}
@Override
public void setBounds(int x,int y,int width,int height)
{
super.setBounds(x,y,width,height);
relayout();
}
private void relayout()
{
int preferredHeight=tf.getPreferredSize().height;
tf.setBounds(0,topOffset,getWidth(),preferredHeight);
}
/**
* @param owner Owner singleton
*/
public EditBoxImp(UISingleton owner)
{
setLayout(null);
add(tf);
owner.informThemeListener(this);
if(defaultFG==null)
{
defaultFG=tf.getForeground();
defaultBG=tf.getBackground();
}
tf.setDocument(new MyDocument());
tf.addActionListener(this);
tf.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();
}
});
tf.addFocusListener(this);
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"pageup");
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"pagedown");
tf.getActionMap().put("pageup",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
if(tvi!=null) tvi.pageUp();
}
});
tf.getActionMap().put("pagedown",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
if(tvi!=null) tvi.pageDown();
}
});
tf.addCaretListener(new CaretListener()
{
@Override
public void caretUpdate(CaretEvent e)
{
forgetTab();
}
});
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"historyup");
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"historydown");
tf.getActionMap().put("historyup",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
if(historyPos>0)
{
historyPos--;
tf.setText(history.get(historyPos));
}
else if(historyPos==-1 && history.size()>0)
{
historyPos=history.size()-1;
history.addLast(tf.getText()); // Store current text...
tf.setText(history.get(historyPos));
}
// Else do nothing, no history or at the top
}
});
tf.getActionMap().put("historydown",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
if(historyPos>=0 && historyPos < history.size()-1)
{
historyPos++;
tf.setText(history.get(historyPos));
if(historyPos==history.size()-1) // Back to 'current' text
{
historyPos=-1;
history.removeLast();
}
}
}
});
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,0),"tabComplete");
tf.getActionMap().put("tabComplete",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
tabComplete();
}
});
tf.getActionMap().put("wipeLine",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
if(!tf.getText().equals(""))
{
wiped=tf.getText();
tf.setText("");
if(wipeInfoOpacity==0)
{
wipeInfoOpacity=255;
(new Thread(new Runnable() {
@Override
public void run()
{
while(true)
{
tf.repaint();
if(wipeInfoOpacity==0) break;
wipeInfoOpacity-=16;
if(wipeInfoOpacity<0) wipeInfoOpacity=0;
try
{
Thread.sleep(50);
}
catch(InterruptedException ie)
{
}
}
}
})).start();
}
}
}
});
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0),"wipeLine");
tf.getActionMap().put("unWipeLine",new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent e)
{
tf.setText(wiped);
}
});
tf.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,InputEvent.SHIFT_MASK),"unWipeLine");
}
/** Wiped line or null if none */
private String wiped;
/** Information about how to un-wipe, fade level */
private int wipeInfoOpacity=0;
/** Cursor as it was on first tab press */
private int tabCursor;
/** Partial text that we're completing */
private String tabPartial;
/** Options for current tab sequence */
private String[] tabOptions;
/** Current option in tab sequence */
private int tabOption;
private boolean inTabStuff=false;
private void forgetTab()
{
if(!inTabStuff)
{
tabOptions=null;
}
}
private void tabComplete()
{
if(tc==null) return;
int cursor=tf.getCaretPosition();
String text=tf.getText();
if(tabOptions==null)
{
// First time. Find partial word before cursor
tabCursor=cursor;
tabPartial=text.substring(0,cursor).replaceAll("^.*[ ,]","");
if(tabPartial.length()==0) return;
// Complete word
tabOptions=tc.complete(tabPartial,tabPartial.length()==text.length());
if(tabOptions.length==0 || tabOptions.length>10)
{
tabOptions=null;
return;
}
tabOption=-1;
}
// Restore
try
{
inTabStuff=true;
// Get rid of previous option
if(tabOption!=-1)
{
int trim=tabOptions[tabOption].length()-tabPartial.length();
tf.getDocument().remove(tabCursor,trim);
}
// Get rid of partial string too
tf.getDocument().remove(tabCursor-tabPartial.length(),tabPartial.length());
// Incremement option
tabOption++;
if(tabOption>=tabOptions.length)
{
// On repeating cycle, go back to original partial
tabOption=-1;
tf.getDocument().insertString(tabCursor-tabPartial.length(),tabPartial,null);
tf.setCaretPosition(tabCursor);
}
else
{
// Add new text and position cursor after it
tf.getDocument().insertString(tabCursor-tabPartial.length(),tabOptions[tabOption],null);
tf.setCaretPosition(tabCursor-tabPartial.length()+tabOptions[tabOption].length());
}
}
catch(BadLocationException e)
{
throw new Error(e);
}
finally
{
inTabStuff=false;
}
}
EditBox getInterface() { return ebi; }
class EditBoxInterface extends BasicWidget implements EditBox,InternalWidget
{
@Override
public int getContentType() { return CONTENT_NONE; }
@Override
public void setOnEnter(String sEnter)
{
getInterface().getOwner().getCallbackHandler().check(sEnter);
EditBoxImp.this.onEnter=sEnter;
}
@Override
public void setOnMultiLine(String callback)
{
getInterface().getOwner().getCallbackHandler().check(callback,new Class[] {String.class});
EditBoxImp.this.onMultiLine=callback;
}
@Override
public void setOnChange(String sChange)
{
getInterface().getOwner().getCallbackHandler().check(sChange);
EditBoxImp.this.onChange=sChange;
}
@Override
public void setOnFocus(String sFocus)
{
getInterface().getOwner().getCallbackHandler().check(sFocus);
EditBoxImp.this.onFocus=sFocus;
}
@Override
public JComponent getJComponent()
{
return EditBoxImp.this;
}
@Override
public int getPreferredWidth()
{
return defaultWidth;
}
@Override
public int getPreferredHeight(int iWidth)
{
return tf.getPreferredSize().height+topOffset;
}
@Override
public void addXMLChild(String sSlotName, Widget wChild)
{
throw new BugException("Edit boxes cannot contain children");
}
@Override
public String getValue()
{
return tf.getText();
}
@Override
public String[] getValueLines()
{
if(lineWrap && lineBreaks!=null)
{
// Not sure why this is called
updateWrap();
// updateWrap can set lineBreaks back to null (no I don't know why it
// would've not been called already with the same data, probably same
// reason it's called here, maybe to do with pasting in?)
if(lineBreaks==null)
{
return new String[] {getValue()};
}
String sValue=getValue();
String[] as=new String[lineBreaks.length+1];
for(int i=0;i<as.length;i++)
{
as[i]=sValue.substring(i==0 ? 0 : lineBreaks[i-1],
i>=lineBreaks.length ? sValue.length() : lineBreaks[i]).trim();
}
return as;
}
else
{
return new String[]{getValue()};
}
}
@Override
public void setValue(String s)
{
try
{
inSet=true;
tf.setText(s);
}
finally
{
inSet=false;
}
}
@Override
public void setEnabled(boolean bEnabled)
{
tf.setEnabled(bEnabled);
}
@Override
public boolean isEnabled()
{
return tf.isEnabled();
}
@Override
public void focus()
{
getUI().focus(tf);
}
@Override
public void setTextView(final String sID)
{
try
{
tvi=((TextViewImp.TextViewInterface)getOwner().getWidget(sID)).getImp();
tvi.informLinked(tf);
}
catch(ClassCastException cce)
{
throw new BugException("<editbox>: TextView= points to something other than a TextView");
}
}
private int flag=FLAG_NORMAL;
@Override
public void setFlag(int iFlag)
{
switch(iFlag)
{
case FLAG_DIM:
tf.setForeground(new Color(defaultFG.getRed(),defaultFG.getGreen(),defaultFG.getBlue(),128));
tf.setBackground(defaultBG);
break;
case FLAG_ERROR:
tf.setForeground(defaultFG);
if(tf.getText().length()>0) // Don't show error flag for blank fields
tf.setBackground(errorBG);
break;
default:
tf.setForeground(defaultFG);
tf.setBackground(defaultBG);
break;
}
this.flag=iFlag;
}
@Override
public int getFlag()
{
return flag;
}
@Override
public void setLineBytes(int iMax)
{
EditBoxImp.this.lineMax=iMax;
}
@Override
public void setLineWrap(boolean bAllowWrap)
{
if(EditBoxImp.this.lineWrap==bAllowWrap) return;
EditBoxImp.this.lineWrap=bAllowWrap;
updateWrap();
}
@Override
public void setWidth(int width)
{
defaultWidth=width;
}
@Override
public void setTabCompletion(TabCompletion tc)
{
EditBoxImp.this.tc=tc;
if(tc==null)
{
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,null);
}
else
{
// Remove tab from the set
Set<AWTKeyStroke> s = new HashSet<AWTKeyStroke>(
getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
s.remove(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB,0,false));
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,s);
}
}
@Override
public void selectAll()
{
tf.selectAll();
}
@Override
public void setRequire(String require)
{
EditBoxImp.this.require=require;
checkRequire();
}
@Override
public void setBaseGroup(String group)
{
if(baseGroup!=null)
{
BaseGroup.Updater.removeFromGroup(EditBoxImp.this,baseGroup);
baseGroup=null;
}
if(group!=null)
{
baseGroup=group;
BaseGroup.Updater.addToGroup(EditBoxImp.this,baseGroup);
}
}
@Override
public void setUseFontSettings(boolean useFontSettings)
{
EditBoxImp.this.useFontSettings=useFontSettings;
resetFont();
}
@Override
public void setRemember(String category, String memoryId)
{
// Get preferences group for this edit box
PluginContext context = getUI().getPluginContext();
Preferences p = context.getSingle(Preferences.class);
prefsGroup = p.getGroup(context.getPlugin()).getChild(
"command-history").getChild(category).getChild(p.getSafeToken(memoryId));
// Set up history based on data from group
for(int i=0; i<MAXHISTORY; i++)
{
String value = prefsGroup.get("l" + i, null);
if(value == null)
{
break;
}
history.addLast(value);
}
}
@Override
public void informClosed()
{
// When closed, save history
if(prefsGroup == null)
{
return;
}
// Clear existing history
for(int i=0; i<MAXHISTORY; i++)
{
prefsGroup.unset("l" + i);
}
// Set new history
int index = 0;
for(String value : history)
{
prefsGroup.set("l" + (index++), value);
}
}
}
@Override
public void actionPerformed(ActionEvent e)
{
// There's an error I can't reproduce which seems like it's possible for
// this to be called after the editbox has been hidden (maybe if you sent
// two CRs very quickly, and the first one closes it - but I can't make
// this happen)
if(!tf.isShowing())
{
return;
}
if(onEnter!=null)
{
if(historyPos!=-1)
{
historyPos=-1;
history.removeLast();
}
history.addLast(tf.getText());
if(history.size() > MAXHISTORY) history.removeFirst();
getInterface().getOwner().getCallbackHandler().callHandleErrors(onEnter);
}
else
{
JButton defaultButton=tf.getRootPane().getDefaultButton();
if(defaultButton!=null) defaultButton.doClick(100);
}
}
private void updateWrap()
{
// If text is longer than the line limit...
try
{
String text=tf.getText();
int totalBytes=text.getBytes("UTF-8").length;
if(lineWrap && lineMax!=EditBox.LINEBYTES_NOLIMIT &&
totalBytes > lineMax)
{
List<Integer> breaks = new LinkedList<Integer>();
int bytes=0,lastWhitespace=-1;
for(int pos=0;pos<text.length();pos++)
{
char thisChar=text.charAt(pos);
if(Character.isWhitespace(thisChar)) lastWhitespace=pos;
int thisBytes=(""+thisChar).getBytes("UTF-8").length;
bytes+=thisBytes;
if(bytes > lineMax)
{
// OK, break at last whitespace if there was one
if(lastWhitespace==-1) lastWhitespace=pos;
breaks.add(lastWhitespace);
pos=lastWhitespace; // We will actually start counting from just after
// because of the for loop, but that's correct
// since the whitespace will be trimmed anyhow
lastWhitespace=-1;
bytes=0;
}
}
/* Old code non-multibyte
int iPos=0;
List lBreaks=new LinkedList();
StringBuffer sb=new StringBuffer(getText());
while(sb.length() > iLineMax)
{
int iBreak=iLineMax;
for(;iBreak>0;iBreak--)
{
if(Character.isWhitespace(sb.charAt(iBreak))) break;
}
if(iBreak==0) iBreak=iLineMax;
lBreaks.add(niBreak+iPos);
iPos+=iBreak;
sb.delete(0,iBreak);
}
*/
int[] newBreaks=new int[breaks.size()];
int index=0;
for(Integer i : breaks)
{
newBreaks[index++] = i;
}
if(!Arrays.equals(newBreaks,lineBreaks))
{
lineBreaks=newBreaks;
repaint();
}
}
else if(lineBreaks!=null)
{
lineBreaks=null;
repaint();
}
}
catch(UnsupportedEncodingException e)
{
throw new BugException(e);
}
}
private void checkRequire()
{
if(require!=null)
{
ebi.setFlag(tf.getText().matches(require) ? EditBox.FLAG_NORMAL : EditBox.FLAG_ERROR);
}
}
private void changed()
{
updateWrap();
forgetTab();
checkRequire();
if(onChange!=null && !inSet)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(onChange);
}
}
@Override
public void focusGained(FocusEvent arg0)
{
if(onFocus!=null)
{
// Defensive programming: it appears possible that sometimes this gets
// called before the owning dialog is fully created. To account for that,
// add an invokeLater. See issue #7.
final WidgetOwner widgetOwner = getInterface().getOwner();
if(widgetOwner.isCreated())
{
widgetOwner.getCallbackHandler().callHandleErrors(onFocus);
}
else
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
if(widgetOwner.isCreated())
{
// Only send event if component is actually visible now. This
// is supposed to account for the situation when the container
// is actually closed before this invokeLater happens.
if(isShowing())
{
widgetOwner.getCallbackHandler().callHandleErrors(onFocus);
}
}
else
{
ErrorMsg.report("Focus in uncreated container", null);
}
}
});
}
}
}
@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 JTextField field;
CutAction(JTextField 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 JTextField field;
CopyAction(JTextField 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 JTextField field;
PasteAction(JTextField 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 class MyTextField extends JTextField
{
private MyAction cut,copy,paste;
private JPopupMenu pm;
MyTextField()
{
// 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);
}
});
}
@Override
public void copy()
{
if(
(tf.getSelectedText()==null || tf.getSelectedText().length()==0)
&& tvi!=null)
{
tvi.copy();
tvi.clearHighlight();
}
else
{
super.copy();
}
}
@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);
// If there are no newlines, go ahead and paste in
if(text.indexOf('\n')==-1)
replaceSelection(text);
else if(onMultiLine!=null) // Multiple lines. Do we support that here?
{
// Can't actually put \n into field and have it keep it, so here's
// a workaround...
String before = getText();
String newLine="--7275lkrbdyh98534--";
replaceSelection(text.replaceAll("\\n",newLine));
String multiLine=getText().replaceAll(newLine,"\n");
setText(before);
getInterface().getOwner().getCallbackHandler().callHandleErrors(onMultiLine,new Object[] {multiLine});
}
else // Just do first line
replaceSelection(text.substring(0,text.indexOf('\n')));
}
@Override
protected void paintComponent(Graphics g)
{
// Default paint
super.paintComponent(g);
// Line breaks
if(lineBreaks!=null)
{
Color
fg=getForeground(),
fg2=new Color(fg.getRed(),fg.getGreen(),fg.getBlue(),166),
fg3=new Color(fg.getRed(),fg.getGreen(),fg.getBlue(),76);
for(int iBreak=0;iBreak<lineBreaks.length;iBreak++)
{
try
{
int iCharacterX=modelToView(lineBreaks[iBreak]).x;
g.setColor(fg);
g.fillRect(iCharacterX,0,1,getHeight());
g.setColor(fg2);
g.fillRect(iCharacterX+1,0,1,getHeight());
g.setColor(fg3);
g.fillRect(iCharacterX+2,0,1,getHeight());
}
catch(BadLocationException ble)
{
assert false;
}
}
}
// Wipe information, a helper string that appears when you press Esc,
// displayed in small font in centre of box.
if(wipeInfoOpacity>0)
{
g.setColor(new Color(255,0,0,wipeInfoOpacity));
Font currentFont=tf.getFont();
Font smallFont=currentFont.deriveFont(currentFont.getSize2D()*0.8f);
g.setFont(smallFont);
String message="[Cleared. Shift+Escape to restore]";
Rectangle2D r=smallFont.getStringBounds(
message,((Graphics2D)g).getFontRenderContext());
// Typically, y is -9.7 and h is 11.8.
g.drawString(message,
(getWidth()-(int)(r.getWidth()+0.5))/2,
(getHeight()- (int)(r.getHeight()+0.5))/2-(int)(r.getY()+0.5));
}
}
}
@Override
public int getBaseline()
{
FontMetrics fm=tf.getFontMetrics(tf.getFont());
int border=(tf.getPreferredSize().height-fm.getHeight())/2;
return border+fm.getAscent();
}
@Override
public InternalWidgetOwner getInternalWidgetOwner()
{
return (InternalWidgetOwner)ebi.getOwner();
}
@Override
public void setTopOffset(int topOffset)
{
if(this.topOffset==topOffset) return;
this.topOffset=topOffset;
relayout();
}
private final static Font defaultTextFieldFont=(new JTextField()).getFont();
/** Update font based on current settings */
private void resetFont()
{
Font f=null;
if(useFontSettings)
{
f=ebi.getUI().getFont();
}
if(f==null)
f=defaultTextFieldFont;
if(!tf.getFont().equals(f))
{
tf.setFont(f);
ebi.redoLayout();
}
}
@Override
public void updateTheme(Theme t)
{
resetFont();
}
// Debugging
@Override
public void paintChildren(Graphics g)
{
super.paintChildren(g);
BaseGroup.Debug.paint(g,this,topOffset);
}
}