/*
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.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import org.w3c.dom.*;
import textlayout.*;
import textlayout.stylesheet.*;
import util.PlatformUtils;
import util.xml.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.*;
import leafchat.core.api.BugException;
/** Implements TextView using the ScrollingLayout class */
public class TextViewImp extends ScrollingLayout implements ThemeListener
{
/** Limit to number of lines */
private int lineLimit=TextView.LINELIMIT_NONE;
/** Scroll callback */
private String sScroll=null;
/** Name in theme */
private String themeType=null;
/** Owner */
private UISingleton owner;
private TextView.MenuHandler mh;
/** Currently marked position */
private int markedPosition=0;
/** Opacity of marker */
private int markOpacity=255;
/** True if a warning should be displayed when scrolled up */
private boolean scrolledUpWarning=false;
/** Map of actions from URL -> ActionHandler */
private Map<String, TextView.ActionHandler> actions =
new HashMap<String, TextView.ActionHandler>();
/**
* Constructs at default size.
* @param owner Owner
*/
public TextViewImp(UISingleton owner)
{
super(200,200,getDefaultStyle(owner,true));
this.owner=owner;
owner.informThemeListener(this);
// Add default URL action
tvi.setAction("url",new TextView.ActionHandler()
{
@Override
public void action(Element e,MouseEvent me)
{
String sURL=XML.getText(e, true, true);
if(!(sURL.startsWith("http://") || sURL.startsWith("https://")))
sURL="http://"+sURL;
try
{
PlatformUtils.showBrowser(new URL(sURL));
}
catch(MalformedURLException e1)
{
ErrorMsg.report("Not a valid URL",e1);
}
catch(IOException e1)
{
ErrorMsg.report(e1.getMessage(),e1.getCause());
}
return;
}
});
}
private static StyleContext contextWithThemes=null,contextWithThemesTV=null;
private static Theme currentTheme;
static StyleContext getDefaultStyle(UISingleton ui,boolean textView)
{
Theme t=ui.getTheme();
if(t==currentTheme)
{
if(textView && contextWithThemesTV!=null)
return contextWithThemesTV;
else if(!textView && contextWithThemes!=null)
return contextWithThemes;
}
try
{
currentTheme=t;
StyleContext creating=new StyleContext(StyleContext.getDefault(true));
Stylesheet[] ss=t.getStylesheets();
for(int i=0;i<ss.length;i++)
{
creating.addStylesheet(ss[i]);
}
if(textView)
{
creating.addStylesheet(new Stylesheet( ui.getSettingsStylesheet()));
contextWithThemesTV=creating;
}
else
{
contextWithThemes=creating;
}
creating.fix();
return creating;
}
catch(Exception e)
{
throw new BugException(e);
}
}
@Override
protected boolean isAction(Node n,int iCharacter)
{
while(n!=null)
{
if((n instanceof Element) && actions.containsKey(n.getNodeName())) return true;
n=n.getParentNode();
}
return false;
}
@Override
protected void doAction(Node n,int iCharacter,MouseEvent me)
{
while(n!=null)
{
if(n instanceof Element)
{
TextView.ActionHandler ah=actions.get(n.getNodeName());
if(ah!=null)
{
try
{
ah.action((Element)n,me);
}
catch(GeneralException ge)
{
ErrorMsg.report("Error carrying out text view action",ge);
}
return;
}
}
n=n.getParentNode();
}
}
@Override
protected JPopupMenu buildMenu(Node n,int character)
{
PopupMenuImp pm=new PopupMenuImp();
if(mh!=null)
{
mh.addItems(pm.getInterface(),n);
}
return pm;
}
@Override
protected void scrollbarChanged()
{
super.scrollbarChanged();
if(sScroll!=null)
{
getInterface().getOwner().getCallbackHandler().callHandleErrors(sScroll);
}
}
TextView getInterface() { return tvi; }
private TextViewInterface tvi=new TextViewInterface();
class TextViewInterface extends BasicWidget implements TextView,InternalWidget
{
@Override
public int getContentType() { return CONTENT_NONE; }
TextViewImp getImp() { return TextViewImp.this; }
@Override
public void setThemeType(String themeType)
{
TextViewImp.this.themeType=themeType;
updateTheme(getUI().getTheme());
}
@Override
public void setDefaultWidth(int iWidth)
{
setPreferredSize(new Dimension(iWidth,getPreferredSize().height));
}
@Override
public void setDefaultHeight(int iHeight)
{
setPreferredSize(new Dimension(getPreferredSize().width,iHeight));
}
@Override
public JComponent getJComponent()
{
return TextViewImp.this;
}
@Override
public int getPreferredWidth()
{
return getPreferredSize().width;
}
@Override
public int getPreferredHeight(int iWidth)
{
return getPreferredSize().height;
}
@Override
public void addXMLChild(String sSlotName, Widget wChild)
{
throw new BugException("TextViews cannot contain children");
}
@Override
public void addPara(String sPara) throws GeneralException
{
addXML("<para>"+sPara+"</para>");
}
@Override
public void addLine(String sLine) throws GeneralException
{
addXML("<line>"+sLine+"</line>");
}
@Override
public void addXML(String xml) throws GeneralException
{
try
{
String tag=themeType==null ? "output" : "theme_"+themeType;
TextViewImp.this.addBlocks(
XML.parse("<"+tag+">"+xml+"</"+tag+">").getDocumentElement());
if(lineLimit!=LINELIMIT_NONE && getNumBlocks() >= lineLimit + (lineLimit/4))
{
markedPosition-=deleteFirstBlocks(lineLimit/4);
if(markedPosition<0) markedPosition=0;
}
}
catch(XMLException e)
{
throw new GeneralException("Invalid XML: "+xml,e);
}
catch(LayoutException e)
{
throw new GeneralException(e);
}
}
@Override
public void setStyleSheet(InputStream is) throws GeneralException
{
try
{
StyleContext sc=new StyleContext(getDefaultStyle(owner,true));
sc.addStylesheet(new Stylesheet(is));
updateStyle(sc);
}
catch(Exception e)
{
// TODO Improve this handling
throw new GeneralException(e);
}
}
@Override
public void setStyleSheet(String styles) throws GeneralException
{
try
{
StyleContext sc=new StyleContext(getDefaultStyle(owner,true));
sc.addStylesheet(new Stylesheet(styles));
updateStyle(sc);
}
catch(Exception e)
{
// TODO Improve this handling
throw new GeneralException(e);
}
}
@Override
public void scrollToEnd()
{
TextViewImp.this.scrollToEnd();
}
@Override
public boolean isAtEnd()
{
return TextViewImp.this.isAtEnd();
}
@Override
public void setOnScroll(String sCallback)
{
getInterface().getOwner().getCallbackHandler().check(sCallback);
TextViewImp.this.sScroll=sCallback;
}
@Override
public void clear()
{
TextViewImp.this.clear();
}
@Override
public void setAction(String tag,ActionHandler ah)
{
actions.put(tag,ah);
}
@Override
public void copy()
{
TextViewImp.this.copy();
}
@Override
public void selectAll()
{
TextViewImp.this.highlightAll();
}
@Override
public void selectNone()
{
TextViewImp.this.clearHighlight();
}
@Override
public boolean hasSelection()
{
return hasHighlight();
}
@Override
public void setMenuHandler(MenuHandler mh)
{
TextViewImp.this.mh=mh;
}
@Override
public void markPosition()
{
if(markedPosition!=0) repaint(); // To get rid of old marker
markedPosition=getLayoutHeight();
markOpacity=255;
}
@Override
public void fadeMark(int opacity)
{
if(markedPosition==0) return;
markOpacity=opacity;
repaint();
}
@Override
public void removeMark()
{
if(markedPosition==0) return;
markedPosition=0;
repaint();
}
@Override
public boolean hasMark()
{
return markedPosition!=0;
}
@Override
public void setLineLimit(int limit)
{
lineLimit=limit;
}
@Override
public void setScrolledUpWarning(boolean enable)
{
if(scrolledUpWarning==enable) return;
scrolledUpWarning=enable;
repaint();
}
}
@Override
public void updateTheme(Theme t)
{
// This is a bit lame, means we'll reload the stylesheet for each textview,
// but I couldn't think of a better way
contextWithThemesTV=null;
currentTheme=null;
if(themeType==null)
{
setMargins(0,0);
}
else
{
setMargins(
t.getIntProperty(themeType,"leftMargin",0),
t.getIntProperty(themeType,"rightMargin",0));
}
try
{
updateStyle(getDefaultStyle(owner,true));
}
catch(LayoutException le)
{
throw new BugException("Error setting textview style",le);
}
repaint();
}
@Override
protected void paintBehind(Graphics g,int width,int height, int startY)
{
// Draw theme background
BufferedImage
bottomLeft=null,bottomRight=null,topLeft=null,topRight=null,
top=null,bottom=null,left=null,right=null;
if(themeType!=null)
{
Theme t=owner.getTheme();
bottomLeft=t.getImageProperty(themeType,"bottomLeftPic",true,null,null);
bottomRight=t.getImageProperty(themeType,"bottomRightPic",true,null,null);
topLeft=t.getImageProperty(themeType,"topLeftPic",true,null,null);
topRight=t.getImageProperty(themeType,"topRightPic",true,null,null);
top=t.getImageProperty(themeType,"topPic",true,null,null);
bottom=t.getImageProperty(themeType,"bottomPic",true,null,null);
left=t.getImageProperty(themeType,"leftPic",true,null,null);
right=t.getImageProperty(themeType,"rightPic",true,null,null);
}
if(top!=null)
{
for(int x=0;x<width;x+=top.getWidth())
g.drawImage(top,x,0,null);
}
if(bottom!=null)
{
for(int x=0;x<width;x+=bottom.getWidth())
g.drawImage(bottom,x,height-bottom.getHeight(),null);
}
if(left!=null)
{
for(int y=0;y<height;y+=left.getHeight())
g.drawImage(left,0,y,null);
}
if(right!=null)
{
for(int y=0;y<height;y+=right.getHeight())
g.drawImage(right,width-right.getWidth(),y,null);
}
if(topLeft!=null)
g.drawImage(topLeft,0,0,null);
if(topRight!=null)
g.drawImage(topRight,width-topRight.getWidth(),0,null);
if(bottomLeft!=null)
g.drawImage(bottomLeft,0,height-bottomLeft.getHeight(),null);
if(bottomRight!=null)
g.drawImage(bottomRight,width-bottomRight.getWidth(),height-bottomRight.getHeight(),null);
// Draw position marker if it is not the end
if(markedPosition!=0 && getLayoutHeight()!=markedPosition)
{
g.setColor(new Color(
SwitchBar.attentionRGB.getRed(),
SwitchBar.attentionRGB.getGreen(),
SwitchBar.attentionRGB.getBlue(),
markOpacity
));
Graphics2D g2=(Graphics2D)g;
g2.setStroke(new BasicStroke(1,
BasicStroke.CAP_SQUARE,BasicStroke.JOIN_MITER,10,new float[] {1,2},0));
g2.drawLine(1,markedPosition-startY,width,markedPosition-startY);
}
// Draw scrolled-up warning if enabled
if(scrolledUpWarning)
{
int distance=getDistanceFromEnd();
if(distance>0)
{
int value=Math.min(distance,16);
for(int i=0;i<4;i++)
{
g.setColor(new Color(
SwitchBar.attentionRGB.getRed(),
SwitchBar.attentionRGB.getGreen(),
SwitchBar.attentionRGB.getBlue(),
(4-i)*value));
g.fillRect(0,height-1-i,width,1);
}
}
}
}
}