package swingjs.plaf; import jsjava.awt.AWTEvent; import jsjava.awt.Color; import jsjava.awt.Component; import jsjava.awt.Dimension; import jsjava.awt.Font; import jsjava.awt.FontMetrics; import jsjava.awt.Graphics; import jsjava.awt.GraphicsConfiguration; import jsjava.awt.Image; import jsjava.awt.Insets; import jsjava.awt.Point; import jsjava.awt.Rectangle; import jsjava.awt.Toolkit; import jsjava.awt.event.FocusEvent; import jsjava.awt.event.PaintEvent; import jsjava.awt.image.ColorModel; import jsjava.awt.image.ImageObserver; import jsjava.awt.image.ImageProducer; import jsjava.awt.image.VolatileImage; import jsjava.awt.peer.ContainerPeer; import jsjava.awt.peer.LightweightPeer; import jsjavax.swing.AbstractButton; import jsjavax.swing.JComponent; import jsjavax.swing.JRootPane; import jsjavax.swing.plaf.ComponentUI; import jssun.awt.CausedFocusEvent.Cause; import swingjs.JSToolkit; import swingjs.api.DOMNode; import swingjs.api.JQueryObject; /** * The JSComponentUI subclasses are where all the detailed HTML5 implementation is * carried out. These subclasses mirror the subclasses found in the actual javax.swing.plaf * but have an important difference in that that effectively act as both the UI (a single * implementation for a given AppContext in Swing) and a peer (one implementation per component). * * So here we store both the constants for the HTML5 "LookAndFeel", but also * HTML5 objects that really are on the page. * * Essentially, at least for now, we are not implementing the HTML5LookAndFeel as such. We'll see how that goes. * * * * @author Bob Hanson * */ public abstract class JSComponentUI extends ComponentUI implements JSEventHandler { /** * provides a unique id for any component; set on instantiation */ protected static int incr; /** * a unique id */ protected String id; /** * the associated JComponent; for which this is c.ui * */ protected JComponent c; /** * the outermost div holding a component -- left, top, and for a container width and height */ protected DOMNode outerNode; /** * the main object for the component, possibly containing others, such as radio button with its label */ protected DOMNode domNode; /** * a component or subcomponent that can be enabled/disabled */ protected DOMNode enableNode; /** * the part of a component that can hold text */ protected DOMNode textNode; /** * the subcomponent with the value field */ protected DOMNode valueNode; /** * a component that is being scrolled by a JScrollPane */ protected DOMNode scrollNode; /** * a component that is focusable */ protected DOMNode focusNode; /** * DOM components pre-defined (JScrollPane) * */ protected Component[] components; /** * a numerical reference for an ID */ protected int num; /** * not implemented/needed currently. Java handles this nicely * */ protected boolean isTainted = true; /** * left and top coordinates */ protected int x, y; /** * preferred dimension set by user * */ protected Dimension preferredSize; /** * panels * */ protected boolean isContainer; /** * linked nodes of this class * */ protected JSComponentUI parent; String currentText; /** * the scroller for a text area */ protected JSScrollPaneUI scrollerNode; /** * uiClassID for this component */ protected String classID; private DOMNode document, body; protected boolean needPreferred; public JSComponentUI() { setDoc(); } protected void setDoc() { /** * @j2sNative * * this.document = document; * this.body = document.body; */ {} } protected abstract void installJSUI(); protected abstract void uninstallJSUI(); public void installUI(JComponent c) { // already done installJSUI(); } public void uninstallUI(JComponent c) { uninstallJSUI(); } protected JQueryObject $(DOMNode node) { return JSToolkit.getJQuery().$(node); } /** * mark this component as in need of update; * maybe not necessary, though. It comes after the value callback */ public void setTainted() { isTainted = true; } public abstract DOMNode getDOMObject(); public JSComponentUI set(JComponent target) { c = target; newID(); if (needPreferred) getPreferredSize(c); installJSUI(); // need to do this immediately, not later return this; } protected void newID() { classID = c.getUIClassID(); if (id == null) { num = ++incr; id = c.getHTMLName(classID) + "_" + num; } } protected DOMNode setCssFont(DOMNode obj, Font font) { if (font != null) { int istyle = font.getStyle(); String name = font.getFamily(); if (name == "Dialog") name = "Arial"; DOMNode.setStyles(obj, "font-family", name, "font-size", font.getSize() + "px", "font-style", ((istyle & Font.ITALIC) == 0 ? "normal" : "italic"), "font-weight", ((istyle & Font.BOLD) == 0 ? "normal" : "bold")); } if (c.isBackgroundSet()) setBackground(c.getBackground()); setForeground(c.getForeground()); return obj; } protected DOMNode createDOMObject(String key, String id, String... attr) { DOMNode obj = DOMNode.createElement(key, id); for (int i = 0; i < attr.length;) DOMNode.setAttr(obj, attr[i++], attr[i++]); if (!c.isEnabled()) setEnabled(false); return obj; } /** * JSmolCore.js will look for data-UI attribute and, if found, reroute directly here * @param node */ protected void bindMouse(DOMNode node) { DOMNode.setAttr(node, "data-UI", this); } /** * called by JmolCore.js * @return true if handled */ public boolean handleJSEvent(Object target, int eventType, Object jQueryEvent) { //System.out.println(id + " handling event " + eventType + jQueryEvent); return false; } protected DOMNode wrap(String type, String id, DOMNode... elements) { return append(createDOMObject(type, id + type), elements); } protected DOMNode append(DOMNode obj, DOMNode[] elements) { for (int i = 0; i < elements.length; i++) { obj.appendChild(elements[i]); } return obj; } protected void debugDump(DOMNode d) { System.out.println(DOMNode.getAttr(d, "outerHTML")); } protected static void vCenter(DOMNode obj, int offset) { DOMNode.setStyles(obj, "top", "50%", "transform","translateY(" + offset + "%)"); } /** * overloaded to allow panel and radiobutton to handle slightly differently * * @param obj * @param addCSS * @return */ protected Dimension setHTMLSize(DOMNode obj, boolean addCSS) { return setHTMLSize1(obj, addCSS, true); } /** * also called by JSRadioButtonUI so that it can calculate * subset dimensions * * @param node * @param addCSS * @param usePreferred * @return */ @SuppressWarnings("unused") protected Dimension setHTMLSize1(DOMNode node, boolean addCSS, boolean usePreferred) { if (node == null) return null; int h, w; String w0 = null, h0 = null; DOMNode parentNode = null; if (scrollerNode != null) { w = scrollerNode.c.getWidth(); h = scrollerNode.c.getHeight(); } else if (usePreferred && preferredSize != null) { // user has set preferred size w = preferredSize.width; h = preferredSize.height; } else { // determine the natural size of this object // save the parent node -- we will need to reset that. parentNode = DOMNode.remove(node); // remove position, width, and height, because those are what we are // setting here /** * @j2sNative * * w0 = node.style.width; * h0 = node.style.height; */ {} DOMNode.setStyles(node, "position", null, "width", null, "height", null); DOMNode div; if (DOMNode.getAttr(node, "tagName") == "DIV") div = node; else div = wrap("div", id + "_temp", node); DOMNode.setStyles(div, "position", "absolute"); // process of discovering width and height is facilitated using jQuery // and appending to document.body. body.appendChild(div); //System.out.println(DOMNode.getAttr(node, "outerHTML")); w = (int) Math.ceil($(div).width() + 0.5); h = (int) Math.ceil($(div).height() + 0.5); body.removeChild(div); } Dimension size = getCSSDimension(w, h); if (addCSS) { DOMNode.setStyles(node, "position", "absolute"); DOMNode.setSize(node, size.width, size.height); } else { DOMNode.setStyles(node, "position", null); // check to reset width/height after getPreferredSize if (w0 != null) DOMNode.setStyles(node, "width", w0, "height", h0); } if (parentNode != null) parentNode.appendChild(node); //System.out.println("JSComponentUI " + id + " resized to " + w + "x" + h + " parent=" + DOMNode.getAttr(parentNode,"id")); return size; } /** * can be overloaded to allow some special adjustments * * @param w * @param h * @return */ protected Dimension getCSSDimension(int w, int h) { return new Dimension(w, h); } /** * creates the DOM node and inserts it into the tree at the correct place, * iterating through all children if this is a container * * @return * */ protected DOMNode setHTMLElement() { if (!isTainted) return outerNode; // check for root pane -- not included in DOM JRootPane root = (isContainer ? c.getRootPane() : null); if (c == root) { isTainted = false; return outerNode; } domNode = getDOMObject(); // divObj will need recreating if a propertyChange event has occurred // check for content pane -- needs to be added to the HTML5 content layer // div // needs some work for changes after applet creation if (outerNode == null) { outerNode = wrap("div", id, domNode); if (root != null && root.getContentPane() == c) swingjs.JSToolkit.getHTML5Applet(c)._getContentLayer() .appendChild(outerNode); } // set position DOMNode.setStyles(outerNode, "position", "absolute", "left", (x = c.getX()) + "px", "top", (y = c.getY()) + "px"); if (isContainer) { // set width from component System.out.println("JSComponentUI container " + id + " " + c.getBounds()); DOMNode.setSize(outerNode, c.getWidth(), c.getHeight()); // add all children Component[] children = (components == null ? c.getComponents() : components); for (int i = children.length; --i >= 0;) { JSComponentUI ui = JSToolkit.getUI(children[i], false); if (ui == null) { // Box.Filler has no ui. continue; } if (ui.outerNode == null) ui.setHTMLElement(); if (ui.outerNode == null) { System.out.println("JSCUI could not add " + ui.c.getName() + " to " + c.getName()); } else { outerNode.appendChild(ui.outerNode); } ui.parent = this; } } // mark as not tainted // debugDump(divObj); isTainted = false; return outerNode; } /** * c ignored because JSComponentUI is one per component */ public Dimension getPreferredSize(JComponent c) { //System.out.println("getPreferredSize for " + id + " " + c.getName()); Dimension d = setHTMLSize(getDOMObject(), false); //System.out.println("JSComponentUI " + id + " getting preferred size as " + d); return d; } public void paint(Graphics g, JComponent c) { // Note that for now, button graphics // are BEHIND the button. We will need to paint onto the // glass pane for this to work, and then also manage // mouse clicks and key clicks with that in mind. if (c.isOpaque()) { g.setColor(c.getBackground()); g.fillRect(0, 0, c.getWidth(), c.getHeight()); } } public void update(Graphics g, JComponent c) { // called from JComponent.paintComponent boolean testing = false;//true; if (testing) { g.setColor(Color.red); g.drawRect(0, 0, c.getWidth(), c.getHeight()); System.out.println("drawing " + c.getWidth() + " " + c.getHeight()); } setHTMLElement(); paint(g, c); } public Dimension getMinimumSize(JComponent c) { return getPreferredSize(c); } public Dimension getMaximumSize(JComponent c) { return null;// getPreferredSize(c); } /** * Returns true if the specified x,y location is * contained within the look and feel's defined shape of the specified * component. x and y are defined to be relative * to the coordinate system of the specified component. Although * a component's bounds is constrained to a rectangle, * this method provides the means for defining a non-rectangular * shape within those bounds for the purpose of hit detection. * * @param c the component where the x,y location is being queried; * this argument is often ignored, * but might be used if the UI object is stateless * and shared by multiple components * @param x the x coordinate of the point * @param y the y coordinate of the point * * @see jsjavax.swing.JComponent#contains * @see jsjava.awt.Component#contains */ public boolean contains(JComponent c, int x, int y) { return c.inside(x, y); } /** * Returns an instance of the UI delegate for the specified component. * Each subclass must provide its own static createUI * method that returns an instance of that UI delegate subclass. * If the UI delegate subclass is stateless, it may return an instance * that is shared by multiple components. If the UI delegate is * stateful, then it should return a new instance per component. * The default implementation of this method throws an error, as it * should never be invoked. */ public static ComponentUI createUI(JComponent c) { // SwingJS so, actually, we don't do this. This class is NOT stateless. // Instead, what we do is to create a unique instance // right in UIManager. The sequence is: // JRadioButton.updateUI() // --> jsjavax.swing.UIManager.getUI(this) // --> jsjavax.swing.UIManager.getDefaults().getUI(target) // --> JSToolkit.getComponentUI(target) // --> creates an instance of JRadioButtonUI and returns // that instance as JRadioButton.ui, which is NOT static. // // throw new Error("ComponentUI.createUI not implemented."); return null; } /** * Returns the baseline. The baseline is measured from the top of * the component. This method is primarily meant for * LayoutManagers to align components along their * baseline. A return value less than 0 indicates this component * does not have a reasonable baseline and that * LayoutManagers should not align this component on * its baseline. *

* This method returns -1. Subclasses that have a meaningful baseline * should override appropriately. * * @param c JComponent baseline is being requested for * @param width the width to get the baseline for * @param height the height to get the baseline for * @throws NullPointerException if c is null * @throws IllegalArgumentException if width or height is < 0 * @return baseline or a value < 0 indicating there is no reasonable * baseline * @see jsjavax.swing.JComponent#getBaseline(int,int) * @since 1.6 */ public int getBaseline(JComponent c, int width, int height) { if (c == null) { throw new NullPointerException("Component must be non-null"); } if (width < 0 || height < 0) { throw new IllegalArgumentException( "Width and height must be >= 0"); } return -1; } /** * Returns an enum indicating how the baseline of he component * changes as the size changes. This method is primarily meant for * layout managers and GUI builders. *

* This method returns BaselineResizeBehavior.OTHER. * Subclasses that support a baseline should override appropriately. * * @param c JComponent to return baseline resize behavior for * @return an enum indicating how the baseline changes as the component * size changes * @throws NullPointerException if c is null * @see jsjavax.swing.JComponent#getBaseline(int, int) * @since 1.6 */ public Component.BaselineResizeBehavior getBaselineResizeBehavior( JComponent c) { if (c == null) { throw new NullPointerException("Component must be non-null"); } return Component.BaselineResizeBehavior.OTHER; } /** * overridden in JSPasswordFieldUI * @return texat */ public String getJSTextValue() { return (String) DOMNode.getAttr(domNode, valueNode == null ? "innerHTML" : "value"); } public void notifyPropertyChanged(String prop) { DOMNode obj = null; String val = null; if (prop == "text") { val = ((AbstractButton) c).getText(); if (val.equals(currentText)) // we set it here, then fired the property change return; currentText = val; if (textNode != null) { prop = "innerHTML"; obj = textNode; } else if (valueNode != null) { prop = "value"; obj = valueNode; } } else if (prop == "preferredSize") { preferredSize = c.getPreferredSize(); // may be null getPreferredSize(); return; } if (obj == null) { System.out.println("JSComponentUI: unrecognized prop: " + prop); } else { System.out.println("JSComponentUI: setting " + id + " " + prop);// + " " + val); setProp(obj, prop, val); } } protected DOMNode setProp(DOMNode obj, String prop, String val) { return DOMNode.setAttr(obj, prop, val); } @Override public boolean isObscured() { JSToolkit.notImplemented(""); return false; } @Override public boolean canDetermineObscurity() { JSToolkit.notImplemented(""); return false; } @Override public void setVisible(boolean b) { DOMNode.setStyles(outerNode, "display", b ? "block" : "none"); } @Override public void setEnabled(boolean b) { if (enableNode != null) DOMNode.setAttr(enableNode, "disabled", (b ? null : "TRUE")); } @Override public void paint(Graphics g) { // nothing to do here } @Override public void repaint(long tm, int x, int y, int width, int height) { // nothing to do here } @Override public void print(Graphics g) { JSToolkit.notImplemented(""); } @Override public void setBounds(int x, int y, int width, int height, int op) { switch (op) { case SET_SIZE: case SET_BOUNDS: case SET_CLIENT_SIZE: if (scrollerNode != null) { width = Math.min(width, scrollerNode.c.getWidth()); height = Math.min(height, scrollerNode.c.getHeight()); } System.out.println(id + " setBounds " + x + " " + y + " " + width + " " + height + " op=" + op); if (domNode != null) DOMNode.setSize(domNode, width, height); break; } } @Override public void handleEvent(AWTEvent e) { JSToolkit.notImplemented(""); } @Override public void coalescePaintEvent(PaintEvent e) { JSToolkit.notImplemented(""); } /** * Coordinates relative to the document * */ @Override public Point getLocationOnScreen() { Insets offset = (Insets) $(outerNode).offset(); return new Point(offset.left, offset.top); } @Override public Dimension getPreferredSize() { return getPreferredSize(c); } @Override public Dimension getMinimumSize() { JSToolkit.notImplemented(""); return getPreferredSize(c); } @Override public ColorModel getColorModel() { return Toolkit.getDefaultToolkit().getColorModel(); } @Override public Toolkit getToolkit() { return Toolkit.getDefaultToolkit(); } @Override public Graphics getGraphics() { // n/a -- called from java.awt.Component when NOT a LightweightPeer. return null; } @Override public FontMetrics getFontMetrics(Font font) { return c.getFontMetrics(font); } @Override public void dispose() { JSToolkit.notImplemented(""); } @Override public void setForeground(Color color) { if (domNode != null) DOMNode.setStyles(domNode, "color", JSToolkit.getCSSColor(color == null ? Color.black : color)); } @Override public void setBackground(Color color) { if (domNode != null) DOMNode.setStyles(domNode, "background-color", JSToolkit.getCSSColor(color == null ? Color.white : color)); } @Override public void setFont(Font f) { if (domNode != null) setCssFont(domNode, f); } @Override public void updateCursorImmediately() { JSToolkit.notImplemented(""); } @Override public boolean requestFocus(Component lightweightChild, boolean temporary, boolean focusedWindowChangeAllowed, long time, Cause cause) { if (focusNode == null) return false; $(focusNode).focus(); if (textNode != null) $(textNode).select(); return true; } @Override public boolean isFocusable() { return (focusNode != null); } @Override public Image createImage(ImageProducer producer) { JSToolkit.notImplemented(""); return null; } @Override public Image createImage(int width, int height) { JSToolkit.notImplemented(""); return null; } @Override public VolatileImage createVolatileImage(int width, int height) { JSToolkit.notImplemented(""); return null; } @Override public boolean prepareImage(Image img, int w, int h, ImageObserver o) { JSToolkit.notImplemented(""); return false; } @Override public int checkImage(Image img, int w, int h, ImageObserver o) { JSToolkit.notImplemented(""); return 0; } @Override public GraphicsConfiguration getGraphicsConfiguration() { JSToolkit.notImplemented(""); return null; } @Override public boolean handlesWheelScrolling() { JSToolkit.notImplemented(""); return false; } @Override public Image getBackBuffer() { JSToolkit.notImplemented(""); return null; } @Override public void destroyBuffers() { JSToolkit.notImplemented(""); } @Override public void reparent(ContainerPeer newContainer) { JSToolkit.notImplemented(""); } @Override public boolean isReparentSupported() { JSToolkit.notImplemented(""); return false; } @Override public void layout() { JSToolkit.notImplemented(""); } @Override public Rectangle getBounds() { JSToolkit.notImplemented(""); return null; } public boolean hasFocus() { return focusNode != null && focusNode == DOMNode.getAttr(document, "activeElement"); } public void notifyFocus(boolean focusGained) { Toolkit.getEventQueue().postEvent(new FocusEvent(c, focusGained ? FocusEvent.FOCUS_GAINED : FocusEvent.FOCUS_LOST)); } }