/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2003-2004 Pierre G. Richard * * Copyright (C) 2003-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine 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. * * * * This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 * * A copy of this license is located in file license.txt at the root of this * * SDK or can be downloaded here: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * * *********************************************************************************/ package totalcross.ui.html; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //!!!! REMEMBER THAT ANY CHANGE YOU MAKE IN THIS CODE MUST BE SENT BACK TO SUPERWABA COMPANY !!!! //!!!! LEMBRE-SE QUE QUALQUER ALTERACAO QUE SEJA FEITO NESSE CODIGO DEVER� SER ENVIADA PARA NOS !!!! //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! import totalcross.net.HttpStream; import totalcross.net.URI; import totalcross.sys.*; import totalcross.ui.*; import totalcross.ui.event.*; import totalcross.ui.gfx.Graphics; import totalcross.ui.image.Image; import totalcross.ui.image.ImageException; import totalcross.util.*; import totalcross.xml.*; /** Represents a Html Document. * Here is a sample of how to load an image of a document instead of downloading it from * a URL: * <pre> * // Extends Document so that we can provide another way to load images, from our internal database * class ArticleDocument extends Document * { * PDBFile cat; * DataStream ds; * * /** The cat parameter should be an already open PDBFile which contains all the images. * * Note that the image is in the form "<rec number>.gif" or "<rec number>.jpeg". * * / * public ArticleDocument(XmlReadable doc, PDBFile cat) * { * this.cat = cat; * ds = new DataStream(cat); * renderDoc(doc); * } * * protected Image loadImage(String src, URI base) * { * int dot = src.indexOf('.'); * if (dot == -1) throw new RuntimeException("src image not in expected format: "+src); * int idx = Convert.toInt(src.substring(0,dot)); * String ext = src.substring(dot+1).toLowerCase(); * if (cat.setRecordPos(idx)) * { * int size = cat.getRecordSize(); * byte []data = new byte[size]; * ds.readBytes(data); * return new Image(new ByteArrayStream(data)); * } * } * } * </pre> * And here's a sample on how to call it: * <pre> * String s = ...; // get the html from somewhere and store in a String * if (!s.substring(0,10).toLowerCase().startsWith("<body")) // make sure we are between a body tag * s = "<body>"+s+"</body>"; * // Displays the xml * hcView.setDocument(new ArticleDocument(new XmlReadableString(s),cat)); // cat must be already instantiated * </pre> * To change the font size, you must change Style.defaultFontSize. To change the default colors, * change UIColors.htmlXXX fields, and don't forget to call htmlContainer.setBackForeColors with these colors. * @see Style#defaultFontSize * @see totalcross.ui.UIColors#htmlContainerControlsBack * @see totalcross.ui.UIColors#htmlContainerControlsFore * @see totalcross.ui.UIColors#htmlContainerLink */ public class Document extends ScrollContainer { /** Base URL for this document */ public URI baseURI; /** Title associated with this document. */ public String title=""; /** All forms in this document. */ public Vector vForms = new Vector(2); /** * The password mode for the Edits. Defaults to Edit.PASSWORD_ALL. * * @see totalcross.ui.Edit#PASSWORD * @see totalcross.ui.Edit#PASSWORD_ALL */ public byte passwordMode = Edit.PASSWORD_ALL; /** * Constructor * * @param doc XmlReadable to be read * @throws totalcross.io.IOException */ public Document(XmlReadable doc) throws totalcross.io.IOException, SyntaxException { renderDoc(doc); } /** * Default Constructor. The document must be rendered using <code>renderDoc</code>. */ protected Document() // guich@510_22 { } /** * Can be overridden by the caller in order to load an image from a different place. * Must return null if not handled/not found. * By default, searches for the given file at the loaded libraries (TCZ files). * @param src The image name * @param baseURI The base URI * @return an Image */ protected Image loadImage(String src, URI baseURI) // guich@510_20 { try { return new Image(src); } catch (Exception e) { e.printStackTrace(); } return null; } /** Renders the document. * @param doc XmlReadable to be read * @throws totalcross.io.IOException */ protected void renderDoc(XmlReadable doc) throws totalcross.io.IOException, SyntaxException // guich@510_22 { doc.setCaseInsensitive(true); baseURI = doc.getBaseURI(); backColor = new Builder(doc).currStyle.backColor; } static class Hidden extends Control { public Hidden() {focusTraversable = false;} public int getPreferredWidth() {return 0;} public int getPreferredHeight() {return 0;} } private class Img extends Control implements Document.CustomLayout, Document.SizeDelimiter { /** * read align attribute once */ private int align; public int valign; private String altName; private Image img; /** * Constructor * * @param doc containing document * @param atts tag attributes * @param style associated style */ Img(Style currStyle, AttributeList atts, URI baseURI) { String v; boolean mustBeScaled = false; v = atts.getAttributeValue("align"); this.align = v == null ? currStyle.getControlAlignment(false) : v.equalsIgnoreCase("center") ? CENTER : v.equalsIgnoreCase("right") ? RIGHT : LEFT; v = atts.getAttributeValue("valign"); this.valign = v == null ? TOP : v.equalsIgnoreCase("middle") || v.equalsIgnoreCase("center") ? CENTER : v.equalsIgnoreCase("bottom") ? BOTTOM : TOP; this.altName = atts.getAttributeValue("alt"); int width= atts.getAttributeValueAsInt("width",0); int height=atts.getAttributeValueAsInt("height",0); mustBeScaled = width > 0 || height > 0; String src = atts.getAttributeValue("src"); if (src != null) try { img = Document.this.loadImage(src, baseURI); // let our caller handle the image - maybe it comes from another source? if (img == null) { URI uri = new URI(src, baseURI); HttpStream in = new HttpStream(uri); if (in.isOk()) img = in.makeImage(); } if (img != null) { int imgWidth, imgHeight; if ( (imgWidth = img.getWidth()) <= 0 || (imgHeight = img.getHeight()) <= 0) img = null; else if (mustBeScaled) img = (height <= 0 && (height=(imgHeight*width)/imgWidth) <= 0) || (width <= 0 && (width=(imgWidth*height)/imgHeight) <= 0) ? null : img.getSmoothScaledInstance(width, height); } } catch (Exception e) { img = null; if (Settings.onJavaSE) Vm.warning("Error reading image: "+e.getMessage()); } if (img == null) try { String s = altName != null ? altName : src != null ? src : "no image"; int tw = fm.stringWidth(s); img = new Image(Math.max(width,tw)+4,Math.max(height,fmH)+3); int w = img.getWidth(); int h = img.getHeight(); Graphics g = img.getGraphics(); g.backColor = UIColors.htmlContainerControlsBack; g.fillRect(0,0,w,h); g.foreColor = UIColors.htmlContainerControlsFore; g.drawRect(0,0,w,h); g.foreColor = UIColors.htmlContainerControlsFore; g.drawText(s, (w-tw)/2+1,(h-fmH)/2); } catch (ImageException e) {} focusTraversable = currStyle.href != null; } public int getMaxWidth() { return img.getWidth(); } public int getPreferredWidth() { return img.getWidth(); } public int getPreferredHeight() { return img.getHeight(); } public void layout(LayoutContext lc) { int width = img.getWidth(); int height = img.getHeight(); int alignedPosX = lc.nextX; int alignedPosY = lc.nextY; //mike@570_59 implemented layouting accordingly to the alignment of the image switch (align) { case CENTER: alignedPosX += (lc.parentContainer.getWidth() - width) / 2; break; case RIGHT: alignedPosX += lc.parentContainer.getWidth() - width; break; case LEFT: // default, left aligned break; } switch (valign) { case CENTER: alignedPosY = (parent.getHeight() - alignedPosY - height) / 2; break; case BOTTOM: alignedPosY += parent.getHeight() - height; break; case TOP: // default, top aligned break; } lc.verify(img.getWidth()); setRect(alignedPosX,alignedPosY,PREFERRED,PREFERRED); lc.update(width); lc.incY = Math.max(lc.incY,img.getHeight()-fmH); } public void onPaint(Graphics g) { g.drawImage(img,0,0); } /** added support for links on images */ public void onEvent(Event e) // mike@570_59 { Style s; if ((e.type == PenEvent.PEN_DOWN || e.type == KeyEvent.ACTION_KEY_PRESS) && (s=ControlProperties.getStyle(this)).href != null) HtmlContainer.getHtmlContainer(this).postLinkEvent(s.href); } } static class BR extends Control implements CustomLayout { private boolean isP; public BR(boolean isP) { this.isP = isP; focusTraversable = false; } public int getPreferredWidth() { return 0; } public int getPreferredHeight() { return fmH+(isP?Edit.prefH:0); } public void layout(LayoutContext lc) { setRect(lc.nextX,lc.nextY,PREFERRED,PREFERRED); lc.disjoin(); } } static class Entry { String key, value; Entry(String key, String value) { this.key = key; this.value = value; } public String toString() { return value; } public boolean equals(Object arg0) //flsobral@tc114_92: added method equals to class totalcross.ui.html.Document.Entry, to compare only the entry value. This fixes an issue with Form.setValue. { return arg0.equals(value); } } static class SubmitReset implements PressListener { boolean isReset; Form currForm; String title; public SubmitReset(Form currForm, boolean isReset, String title) { this.title = title; this.isReset = isReset; this.currForm = currForm; } public void controlPressed(ControlEvent e) { if (isReset) currForm.reset(); else currForm.submit(title); } } private class Builder extends XmlReader { private Style currStyle; private Form currForm; Builder(XmlReadable doc) throws totalcross.io.IOException, SyntaxException { currStyle = new Style(); Document.this.width= Document.this.height = 4096; new HeadContentHandler(); disableReferenceResolution(true); parse(doc); } /** Impl. note: only works when reference resolution has been disabled */ protected void foundReference(byte input[], int offset, int count) { char res = NamedEntitiesDereferencer.toCode(input, offset, count); if (res == 0) res = super.resolveCharacterReference(input, offset, count); foundCharacter(res); } protected int getTagCode(byte b[], int offset, int count) { return TagDereferencer.toCode(b, offset, count); } public final void foundStartTagName(byte buffer[], int offset, int count) { super.foundStartTagName(buffer, offset, count); switch (tagNameHashId) { case TagDereferencer.SCRIPT: case TagDereferencer.TEXTAREA: setCdataContents(buffer, offset, count); break; case TagDereferencer.PRE: case TagDereferencer.XMP: setNewlineSignificant(true); break; } } public final void foundEndTagName(byte buffer[], int offset, int count) { super.foundEndTagName(buffer, offset, count); switch (tagNameHashId) { case TagDereferencer.PRE: case TagDereferencer.XMP: setNewlineSignificant(false); break; } } private class HeadContentHandler extends ContentHandler { boolean inTitle; HeadContentHandler() { setContentHandler(this); } public void startElement(int tagHashId, AttributeList atts) { switch (tagHashId) { case TagDereferencer.HTML: break; case TagDereferencer.TITLE: inTitle = true; break; case TagDereferencer.BASE: { String href = atts.getAttributeValue("href"); if (href != null) baseURI = new URI(href); break; } case TagDereferencer.BODY: //flsobral@tc126_36: added support to bgcolor attribute in body tag. String bgcolor = atts.getAttributeValue("bgcolor"); if (bgcolor != null) currStyle.backColor = Style.getColor(bgcolor, currStyle.backColor); new BodyContentHandler(); break; } } public void endElement(int tagHashId) { if (tagHashId == TagDereferencer.TITLE) inTitle = false; } public void characters(String s) { if (inTitle) title = s; } } private class BodyContentHandler extends ContentHandler { Container currTile; ContentHandler prevContentHandler; private Hashtable rgTable; // Names and RadioGroup instances private TextSpan currTextSpan; private boolean openIdent; IntVector alignmentStack = new IntVector(); boolean insidePre = false; BodyContentHandler() { alignmentStack.addElement(Style.ALIGN_LEFT); currTile = Document.this; rgTable = new Hashtable(13); prevContentHandler = getContentHandler(); setContentHandler(this); } public void startElement(int tagHashId, AttributeList atts) { int size; Control control = null; currStyle = Style.tagStartFound(currStyle, tagHashId, atts); if (!insidePre && currStyle.alignment == Style.ALIGN_NONE) { try { currStyle.alignment = alignmentStack.peek(); } catch (ElementNotFoundException e) { // ignore } } String name = atts.getAttributeValue("name"); String value = atts.getAttributeValue("value"); boolean glue = false; switch (tagHashId) { case TagDereferencer.CENTER: alignmentStack.push(Style.ALIGN_CENTER); break; case TagDereferencer.P: control = new BR(true); break; case TagDereferencer.BR: control = new BR(false); break; case TagDereferencer.OL: case TagDereferencer.UL: openIdent = true; break; case TagDereferencer.PRE: insidePre = true; currStyle.alignment = Style.ALIGN_NONE; setNewlineSignificant(true); break; case TagDereferencer.HR: control = new Hr(); break; case TagDereferencer.IMG: control = new Img(currStyle, atts, baseURI); break; case TagDereferencer.FORM: vForms.addElement(currForm = new Form(currForm, atts)); break; case TagDereferencer.INPUT: String type = atts.getAttributeValue("type"); if (type == null || type.equalsIgnoreCase("password")) // Edit is the default input { size = atts.getAttributeValueAsInt("size", -1); int maxLen = atts.getAttributeValueAsInt("maxlength", -1); if (size <= 0) size = 10; Edit ed = new Edit(Convert.dup('0', size)); if (maxLen >= 0) ed.setMaxLength(maxLen); ed.clearValueStr = value == null ? "" : value; if (type != null) // password? ed.setMode(passwordMode); control = ed; } else if (type.equalsIgnoreCase("button")) control = new Button(value == null ? " " : value); else if (type.equalsIgnoreCase("submit") || type.equalsIgnoreCase("reset")) { final boolean isReset = type.equalsIgnoreCase("reset"); control = new Button(value == null ? (isReset?"Reset":"Submit") : value); ((Button)control).addPressListener(new SubmitReset(currForm, isReset, value)); } else if (type.equalsIgnoreCase("checkbox")) { glue = true; control = new Check(""); control.clearValueInt = atts.exists("checked") ? 1 : 0; } else if (type.equalsIgnoreCase("radio")) { glue = true; RadioGroupController rg = null; if (name != null) { rg = (RadioGroupController) rgTable.get(name); if (rg == null) rgTable.put(name, rg = new RadioGroupController()); } control = new Radio("", rg); control.clearValueInt = atts.exists("checked") ? 1 : 0; } else if (type.equalsIgnoreCase("hidden")) control = new Hidden(); break; case TagDereferencer.TABLE: { Table table = new Table(atts, currStyle); control = table; new TableContentHandler(table); break; } case TagDereferencer.SELECT: { // if "multiple" is present, its a MultiListBox. If size > 1, is a // listbox with size lines, else, if size <= 1, its a ComboBox size = atts.getAttributeValueAsInt("size", -1); if (atts.exists("multiple")) { control = new MultiListBox(); ((ListBox)control).visibleLines = size == -1 ? 4 : size; } else if (size > 1) { control = new ListBox(); ((ListBox)control).visibleLines = size; } else control = new ComboBox(); // default is a combobox new SelectContentHandler(control); break; } case TagDereferencer.TEXTAREA: { int rows = atts.getAttributeValueAsInt("rows", -1); int cols = atts.getAttributeValueAsInt("cols", -1); String mask = Convert.dup('0', cols <= 0 ? 20 : cols); if (rows <= 0) rows = 3; MultiEdit me = new MultiEdit(mask, rows, 1); me.drawDots = false; new TextAreaContentHandler(me); break; } default: return; } if (control != null) { control.appObj = new ControlProperties(name, value, currStyle); control.setFont(currStyle.getFont()); ((ControlProperties)control.appObj).glue = glue; if (currForm != null && !(control instanceof BR)) // don't add BR/P to the form currForm.inputs.addElement(control); // add this control to the form. currTile.add(control); control.setBackForeColors(UIColors.htmlContainerControlsBack,UIColors.htmlContainerControlsFore); } } public void endElement(int tagHashId) { switch (tagHashId) { case TagDereferencer.CENTER: try { alignmentStack.pop(); } catch (ElementNotFoundException e) { // ignore } break; case TagDereferencer.TABLE: //mike@570_59 notify table about end ((TableContentHandler)getContentHandler()).table.endTable(); currStyle = Style.tagEndFound(currStyle, tagHashId); case TagDereferencer.BODY: case TagDereferencer.SELECT: setContentHandler(prevContentHandler); break; case TagDereferencer.FORM: if (currForm != null) { currForm.reset(); currForm = currForm.previous; } break; case TagDereferencer.P: currTile.add(new BR(false)); break; case TagDereferencer.PRE: currStyle = Style.tagEndFound(currStyle, tagHashId); insidePre = false; break; case TagDereferencer.OL: case TagDereferencer.UL: currTextSpan.finishIdent = true; // no break! default: currStyle = Style.tagEndFound(currStyle, tagHashId); if ((Style.getStyle(tagHashId) & Style.P_AFTER) != 0) // add a <p> after a <Hn> {currTile.add(new BR(false));currTile.add(new BR(false));} break; } } public void characters(String s) { if (!isDataCDATA() && s.trim().length() > 0) //mike@570_59 Don't create textspan tiles for empty texts { currTextSpan = new TextSpan(currTile, s, currStyle); currTextSpan.openIdent = openIdent; openIdent = false; } } } private class TableContentHandler extends BodyContentHandler { private Table table; TableContentHandler(Table table) { // super() set currTile to the Document.rootTile this is a fall-back in case TextSpan are created // while no table cells exist. This garbaged text will show up after the end of the table. super(); this.table = table; } public void startElement(int tagHashId, AttributeList atts) { super.startElement(tagHashId, atts); switch (tagHashId) { case TagDereferencer.TR: table.startRow(atts, currStyle); break; case TagDereferencer.TD: case TagDereferencer.TH: currTile = table.startCell(atts, currStyle); break; } } public void endElement(int tagHashId) { super.endElement(tagHashId); switch (tagHashId) { case TagDereferencer.TR: table.endRow(); // fall thru case TagDereferencer.TD: case TagDereferencer.TH: currTile = Document.this; // reset default document tiles break; } } } private class SelectContentHandler extends BodyContentHandler { private Control select; private AttributeList currOptionAtts; // remembers curr OPTION attributes private StringBuffer sb; // To catenate the option text SelectContentHandler(Control select) { this.select = select; sb = new StringBuffer(64); } private void endCurrentOption() { if (currOptionAtts != null) { String item = sb.toString(); String key = currOptionAtts.getAttributeValue("value"); boolean selected = currOptionAtts.exists("selected"); if (select instanceof MultiListBox) // must come first! { MultiListBox lb = (MultiListBox)select; lb.add(new Entry(key, item)); if (selected) lb.clearValues.addElement(lb.size() - 1); } else if (select instanceof ListBox) { ListBox lb = (ListBox)select; int count = lb.size(); lb.add(new Entry(key, item)); if (selected) lb.setSelectedIndex(lb.clearValueInt = count); } else // ComboBox { ComboBox cb = (ComboBox)select; int count = cb.size(); cb.add(new Entry(key, item)); if (selected || (count == 0)) cb.clearValueInt = count; } sb.setLength(0); currOptionAtts = null; } } public void startElement(int tagHashId, AttributeList atts) { // no other tags except OPTION are permitted within a SELECT if (tagHashId == TagDereferencer.OPTION) { endCurrentOption(); currOptionAtts = new AttributeList(atts); } } public void endElement(int tagHashId) { switch (tagHashId) { case TagDereferencer.SELECT: endCurrentOption(); setContentHandler(prevContentHandler); break; case TagDereferencer.OPTION: endCurrentOption(); break; } } public void characters(String s) { if (currOptionAtts != null) sb.append(s); } } private class TextAreaContentHandler extends BodyContentHandler { private MultiEdit textArea; private StringBuffer sb; // To concatenate the text of the TextArea TextAreaContentHandler(MultiEdit textArea) { this.textArea = textArea; sb = new StringBuffer(128); } public void startElement(int tagHashId, AttributeList atts) { // no tags are permitted within a TEXTAREA (#CDATA element) } public void endElement(int tagHashId) { if (tagHashId == TagDereferencer.TEXTAREA) { textArea.setText(textArea.clearValueStr = sb.toString()); setContentHandler(prevContentHandler); } } public void characters(String s) { sb.append(s); } } } static interface CustomLayout { public void layout(LayoutContext lc); } static interface StopLayout { } static interface SizeDelimiter { public int getMaxWidth(); } static void layout(Control control, LayoutContext lc) { if (control instanceof CustomLayout) ((CustomLayout)control).layout(lc); else if (control.appObj != null) { ControlProperties cp = (ControlProperties)control.appObj; Style style = cp.style; if ((style.hasInitialValues() && style.isDisjoint)) lc.disjoin(); //TextSpan.debug(lc,style,control.toString()); if (control instanceof BR) lc.incY += control.getPreferredHeight(); else { lc.verify(control.getPreferredWidth()); int dif = (control.getFont().fm.height + Edit.prefH - control.getPreferredHeight())/2; // vertically align the controls with their texts if (dif < 0) dif = 0; int alignedPosX = lc.nextX; switch (style.alignment) { case Style.ALIGN_CENTER: alignedPosX += (lc.parentContainer.getWidth() - control.getPreferredWidth()) / 2; break; case Style.ALIGN_RIGHT: alignedPosX += lc.parentContainer.getWidth() - control.getPreferredWidth(); break; case Style.ALIGN_LEFT: // default, left aligned break; } control.setRect(alignedPosX, lc.nextY+dif, Control.PREFERRED, Control.PREFERRED, lc.lastControl); control.getParent().incLastY(-dif); if (!cp.glue) control.getParent().incLastX(control.fm.charWidth(' ')); lc.update(control.getWidth()); lc.lastControl = control; } } if (control instanceof Container && !(control instanceof StopLayout)) { Control[] children = ((Container)control).getChildren(); if (children != null) for (int i = children.length; --i >= 0;) layout(children[i], lc); } } private int getMaxWidth(Control control, int maxWidth) { maxWidth = Math.max(maxWidth, control instanceof SizeDelimiter ? ((SizeDelimiter)control).getMaxWidth() : control.getPreferredWidth()); if (control instanceof Container) { Control[] children = ((Container)control).getChildren(); for (int i = children==null ? 0 : children.length; --i >= 0;) maxWidth = getMaxWidth(children[i], maxWidth); } return maxWidth; } /** Layout the controls of this document. */ public void initUI() { setBackColor(parent.getBackColor()); //flsobral@tc126_36: restore scrolls default colors. sbV.setBackColor(UIColors.controlsBack); sbH.setBackColor(UIColors.controlsBack); sbV.focusTraversable = sbH.focusTraversable = false; sbV.setValue(0); sbH.setValue(0); int maxWidth = getMaxWidth(this,0); resize(maxWidth, 60000); layout(this, new LayoutContext(getClientRect().width-sbV.getPreferredWidth(), this)); resize(); } public void scroll(int dir) { switch (dir) { case RIGHT : sbH.blockScroll(true); break; case LEFT : sbH.blockScroll(false); break; case BOTTOM: sbV.blockScroll(true); break; case TOP : sbV.blockScroll(false); break; } } public void reposition() { super.reposition(false); sbV.reposition(); sbH.reposition(); } void resetWith(String url) // guich@tc114_28 { Hashtable ht = new Hashtable(URI.decode(url.replace('&','\n'))); //flsobral@tc115_3: Decode the received String. for (int i =vForms.size(); --i >= 0;) { Form f = (Form)vForms.items[i]; Vector inputs = f.inputs; for (int j = inputs.size(); --j >= 0;) { Control item = (Control)inputs.items[j]; ControlProperties cl = (ControlProperties)item.appObj; String name = cl.name,v; if (name != null && (v=(String)ht.get(name)) != null && v.length() > 0) Form.setValue(item, v); } } } }