zul/src/main/java/org/zkoss/zul/Window.java

Summary

Maintainability
F
4 days
Test Coverage
/* Window.java

    Purpose:
        
    Description:
        
    History:
        Tue May 31 19:29:13     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;

import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.IdSpace;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.SuspendNotAllowedException;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.MaximizeEvent;
import org.zkoss.zk.ui.event.MinimizeEvent;
import org.zkoss.zk.ui.event.OpenEvent;
import org.zkoss.zul.ext.Framable;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;

/**
 * A window.
 *
 * <p>Unlike other elements, each {@link Window} is an independent ID space
 * (by implementing {@link org.zkoss.zk.ui.IdSpace}).
 * It means a window and all its descendants forms a ID space and
 * the ID of each of them is unique in this space.
 * You could retrieve any of them in this space by calling {@link #getFellow}.
 *
 * <p>If a window X is a descendant of another window Y, X's descendants
 * are not visible in Y's space. To retrieve a descendant, say Z, of X, 
 * you have to invoke Y.getFellow('X').getFellow('Z').
 *
 * <p>Events:<br/>
 * onMove, onOpen, onMaximize, onMinimize, and onClose.<br/>
 * Note: to have better performance, onOpen is sent only if a
 * non-deferrable event listener is registered
 * (see {@link org.zkoss.zk.ui.event.Deferrable}).
 * 
 * <p><code>onMaximize</code> and <code>onMinimize</code> are supported. (since 3.5.0)
 *
 * <p><code>onClose</code> is sent when the close button is pressed
 * (if {@link #isClosable} is true). The window has to detach or hide
 * the window. By default, {@link #onClose} detaches the window. To prevent
 * it from detached, you have to call {@link org.zkoss.zk.ui.event.Event#stopPropagation}
 * to prevent {@link #onClose} is called.
 *
 * <p>On the other hand, <code>onOpen</code> is sent when a popup
 * window (i.e., {@link #getMode} is popup) is closed due to user's activity
 * (such as press ESC). This event is only a notification.
 * In other words, the popup is hidden before the event is sent to the server.
 * The application cannot prevent the window from being hidden.
 * 
 * <p>Default {@link #getZclass}: z-window.(since 3.5.0)
 * @author tomyeh
 */
public class Window extends XulElement implements Framable, IdSpace {
    private static final Logger log = LoggerFactory.getLogger(Window.class);
    private static final long serialVersionUID = 20100721L;

    private transient Caption _caption;

    private String _border = "none";
    private String _title = "";
    /** One of MODAL, MODAL_EVENT_THREAD_DISABLED, EMBEDDED, OVERLAPPED, HIGHLIGHTED, POPUP. */
    private int _mode = EMBEDDED;
    /** Used for doModal. */
    private Mutex _mutex = new Mutex();
    /** The style used for the content block. */
    private String _cntStyle;
    /** The style class used for the content block. */
    private String _cntSclass;
    /** How to position the window. */
    private String _pos;
    /** Whether to show a close button. */
    private boolean _closable;
    /** Whether the window is sizable. */
    private boolean _sizable;
    /** Whether to show the shadow. */
    private boolean _shadow = true;

    private boolean _maximizable, _minimizable, _maximized, _minimized;
    private int _minheight = 100, _minwidth = 200;

    /** Embeds the window as normal component. */
    public static final int EMBEDDED = 0;
    /** Makes the window as a modal dialog. once {@link #doModal}
     * is called, the execution of the event processing thread
     * is suspended until one of the following occurs.
     * <ol>
     * <li>{@link #setMode} is called with a mode other than MODAL.</li>
     * <li>Either {@link #doOverlapped}, {@link #doPopup},
     * {@link #doEmbedded}, or {@link #doHighlighted} is called.</li>
     * <li>{@link #setVisible} is called with false.</li>
     * <li>The window is detached from the window.</li>
     * </ol>
     *
     * <p>Note: In the last two cases, the mode becomes {@link #OVERLAPPED}.
     * In other words, one might say a modal window is a special overlapped window.
     *
     * @see #HIGHLIGHTED
     */
    public static final int MODAL = 1;
    //Represent a modal when the event thread is disabled (internal)
    private static final int MODAL_EVENT_THREAD_DISABLED = -100;
    /** Makes the window as overlapped other components.
     */
    public static final int OVERLAPPED = 2;
    /** Makes the window as popup.
     * It is similar to {@link #OVERLAPPED}, except it is auto hidden
     * when user clicks outside of the window.
     */
    public static final int POPUP = 3;
    /** Makes the window as highlighted.
     * Its visual effect is the same as {@link #MODAL}.
     * However, from the server side's viewpoint, it is similar to
     * {@link #OVERLAPPED}. The execution won't be suspended when
     * {@link #doHighlighted} is called.
     *
     * @see #MODAL
     * @see #OVERLAPPED
     */
    public static final int HIGHLIGHTED = 4;

    static {
        addClientEvent(Window.class, Events.ON_CLOSE, 0);
        addClientEvent(Window.class, Events.ON_MOVE, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        addClientEvent(Window.class, Events.ON_SIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        addClientEvent(Window.class, Events.ON_OPEN, CE_IMPORTANT);
        addClientEvent(Window.class, Events.ON_Z_INDEX, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        addClientEvent(Window.class, Events.ON_MAXIMIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        addClientEvent(Window.class, Events.ON_MINIMIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
    }

    public Window() {
        setAttribute("z$is", Boolean.TRUE); //optional but optimized to mean no need to generate z$is since client handles it
    }

    /**
     * @param title the window title (see {@link #setTitle}).
     * @param border the border (see {@link #setBorder}).
     * @param closable whether it is closable (see {@link #setClosable}).
     */
    public Window(String title, String border, boolean closable) {
        this();
        setTitle(title);
        setBorder(border);
        setClosable(closable);
    }

    /**
     * Returns whether the window is maximized.
     * @since 3.5.0
     */
    public boolean isMaximized() {
        return _maximized;
    }

    /**
     * Sets whether the window is maximized, and then the size of the window will depend 
     * on it to show a appropriate size. In other words, if true, the size of the
     * window will count on the size of its offset parent node whose position is
     * absolute (by not {@link #inEmbedded()}) or its parent node. Otherwise, its size
     * will be original size. Note that the maximized effect will run at client's
     * sizing phase not initial phase.
     * 
     * <p>Default: false.
     * @exception UiException if {@link #isMaximizable} is false.
     * @since 3.5.0
     */
    public void setMaximized(boolean maximized) {
        if (_maximized != maximized) {
            if (!_maximizable)
                throw new UiException("Not maximizable, " + this);

            _maximized = maximized;
            if (_maximized) {
                _minimized = false;
                setVisible0(true); //avoid dead loop
            }
            smartUpdate("maximized", _maximized);
        }
    }

    /**
     * Returns whether to display the maximizing button and allow the user to maximize
     * the window. 
     * <p>Default: false.
     * @since 3.5.0
     */
    public boolean isMaximizable() {
        return _maximizable;
    }

    /**
     * Sets whether to display the maximizing button and allow the user to maximize
     * the window, when a window is maximized, the button will automatically
     * change to a restore button with the appropriate behavior already built-in
     * that will restore the window to its previous size.
     * <p>Default: false.
     * 
     * <p>Note: the maximize button won't be displayed if no title or caption at all.
     * @since 3.5.0
     */
    public void setMaximizable(boolean maximizable) {
        if (_maximizable != maximizable) {
            _maximizable = maximizable;
            smartUpdate("maximizable", _maximizable);
        }
    }

    /**
     * Returns whether the window is minimized.
     * <p>Default: false.
     * @since 3.5.0
     */
    public boolean isMinimized() {
        return _minimized;
    }

    /**
     * Sets whether the window is minimized.
     * <p>Default: false.
     * @exception UiException if {@link #isMinimizable} is false.
     * @since 3.5.0
     */
    public void setMinimized(boolean minimized) {
        if (_minimized != minimized) {
            if (!_minimizable)
                throw new UiException("not minimizable, " + this);

            _minimized = minimized;
            if (_minimized) {
                _maximized = false;
                setVisible0(false); //avoid dead loop
            } else
                setVisible0(true);
            smartUpdate("minimized", _minimized);
        }
    }

    /**
     * Returns whether to display the minimizing button and allow the user to minimize
     * the window. 
     * <p>Default: false.
     * @since 3.5.0
     */
    public boolean isMinimizable() {
        return _minimizable;
    }

    /**
     * Sets whether to display the minimizing button and allow the user to minimize
     * the window. Note that this button provides no implementation -- the behavior
     * of minimizing a window is implementation-specific, so the MinimizeEvent
     * event must be handled and a custom minimize behavior implemented for this
     * option to be useful.
     * 
     * <p>Default: false. 
     * <p>Note: the maximize button won't be displayed if no title or caption at all.
     * @see MinimizeEvent
     * @since 3.5.0
     */
    public void setMinimizable(boolean minimizable) {
        if (_minimizable != minimizable) {
            _minimizable = minimizable;
            smartUpdate("minimizable", _minimizable);
        }
    }

    /**
     * Sets the minimum height in pixels allowed for this window. If negative, 100 is assumed.
     * <p>Default: 100. 
     * <p>Note: Only applies when {@link #isSizable()} = true.
     * @since 3.5.0
     */
    public void setMinheight(int minheight) {
        if (minheight < 0)
            minheight = 100;
        if (_minheight != minheight) {
            _minheight = minheight;
            smartUpdate("minheight", _minheight);
        }
    }

    /**
     * Returns the minimum height.
     * <p>Default: 100.
     * @since 3.5.0
     */
    public int getMinheight() {
        return _minheight;
    }

    /**
     * Sets the minimum width in pixels allowed for this window. If negative, 200 is assumed.
     * <p>Default: 200. 
     * <p>Note: Only applies when {@link #isSizable()} = true.
     * @since 3.5.0
     */
    public void setMinwidth(int minwidth) {
        if (minwidth < 0)
            minwidth = 200;
        if (_minwidth != minwidth) {
            _minwidth = minwidth;
            smartUpdate("minwidth", _minwidth);
        }
    }

    /**
     * Returns the minimum width.
     * <p>Default: 200.
     * @since 3.5.0
     */
    public int getMinwidth() {
        return _minwidth;
    }

    /** Returns the caption of this window.
     */
    public Caption getCaption() {
        return _caption;
    }

    /** Returns the border.
     *
     * <p>Default: "none".
     */
    public String getBorder() {
        return _border;
    }

    /** Sets the border (either none or normal).
     *
     * @param border the border. If null, "0" or "false", "none" is assumed.
     * If "true", "normal" is assumed (since 5.0.8).
     */
    public void setBorder(String border) {
        if (border == null || "0".equals(border) || "false".equals(border))
            border = "none";
        else if ("true".equals(border))
            border = "normal";
        if (!Objects.equals(_border, border)) {
            _border = border;
            smartUpdate("border", border);
        }
    }

    /** Enables or disables the border.
     * @param border whether to have a border. If true is specified,
     * it is the same as <code>setBorder("normal")</code>.
     * @since 5.0.8
     */
    public void setBorder(boolean border) {
        setBorder(border ? "normal" : "none");
    }

    /** Returns the title.
     * Besides this attribute, you could use {@link Caption} to define
     * a more sophisticated caption (a.k.a., title).
     * <p>If a window has a caption whose label ({@link Caption#getLabel})
     * is not empty, then this attribute is ignored.
     * <p>Default: empty.
     */
    public String getTitle() {
        return _title;
    }

    /** Sets the title.
     */
    public void setTitle(String title) {
        if (title == null)
            title = "";
        if (!Objects.equals(_title, title)) {
            _title = title;
            smartUpdate("title", title);
        }
    }

    /** Returns the current mode.
     * One of "modal", "embedded", "overlapped", "popup", and "highlighted".
     * @see #getModeType
     */
    public String getMode() {
        return modeToString(_mode);
    }

    private static String modeToString(int mode) {
        switch (mode) {
        case MODAL:
        case MODAL_EVENT_THREAD_DISABLED:
            return "modal";
        case POPUP:
            return "popup";
        case OVERLAPPED:
            return "overlapped";
        case HIGHLIGHTED:
            return "highlighted";
        default:
            return "embedded";
        }
    }

    /** Sets the mode.
     * @since 6.0.0
     */
    public void setMode(Mode mode) {
        setMode(mode.id);
    }
    
    /** Sets the mode to overlapped, popup, modal, embedded or highlighted.
     *
     * <p>Notice: {@link Events#ON_MODAL} is posted if you specify
     * "modal" to this method.
     * Unlike {@link #doModal}, {@link Events#ON_MODAL} is posted, so
     * the window will become modal later (since 3.0.4).
     * In other words, setMode("modal") never suspends the execution
     * of the current thread. On the other hand, {@link #doModal} will
     * suspends the execution if executed in an event listener, or
     * throws an exception if <em>not</em> executed in an event listener.
     *
     * <p>Refer to <a href="http://books.zkoss.org/wiki/ZK_Component_Reference/Containers/Window">Overlapped, Popup, Modal, Highlighted and Embedded</a>
     * for more information.
     *
     * @param name the mode which could be one of
     * "embedded", "overlapped", "popup", "modal", "highlighted".
     * Note: it cannot be "modal". Use {@link #doModal} instead.
     */
    public void setMode(String name) {
        if ("popup".equals(name))
            doPopup();
        else if ("overlapped".equals(name))
            doOverlapped();
        else if ("embedded".equals(name))
            doEmbedded();
        else if ("modal".equals(name)) {
            if (isEventThreadEnabled(false))
                Events.postEvent(Events.ON_MODAL, this, null);
            else
                doModal();
        } else if ("highlighted".equals(name))
            doHighlighted();
        else
            throw new WrongValueException("Unknown mode: " + name);
    }
    
    /** Sets the mode to overlapped, popup, modal, embedded or highlighted.
     *
     * @see #setMode(String)
     */
    public void setMode(int mode) {
        switch (mode) {
        case POPUP:
            doPopup();
            break;
        case OVERLAPPED:
            doOverlapped();
            break;
        case EMBEDDED:
            doEmbedded();
            break;
        case MODAL:
            if (isEventThreadEnabled(false))
                Events.postEvent(Events.ON_MODAL, this, null);
            else
                doModal();
            break;
        case HIGHLIGHTED:
            doHighlighted();
            break;
        default:
            throw new WrongValueException("Unknown mode: " + mode);
        }
    }

    /** Returns the current mode.
     * @see #getMode
     * @see #setMode(Mode)
     * @since 6.0.0
     */
    public Mode getModeType() {
        return toModeType(_mode);
    }

    private static Mode toModeType(int mode) {
        switch (mode) {
        case MODAL:
        case MODAL_EVENT_THREAD_DISABLED:
            return Mode.MODAL;
        case POPUP:
            return Mode.POPUP;
        case OVERLAPPED:
            return Mode.OVERLAPPED;
        case HIGHLIGHTED:
            return Mode.HIGHLIGHTED;
        default:
            return Mode.EMBEDDED;
        }
    }

    /** Returns whether this is a modal dialog.
     */
    public boolean inModal() {
        return _mode == MODAL || _mode == MODAL_EVENT_THREAD_DISABLED;
    }

    /** Returns whether this is embedded with other components (Default).
     * @see #doEmbedded
     */
    public boolean inEmbedded() {
        return _mode == EMBEDDED;
    }

    /** Returns whether this is a overlapped window.
     */
    public boolean inOverlapped() {
        return _mode == OVERLAPPED;
    }

    /** Returns whether this is a popup window.
     */
    public boolean inPopup() {
        return _mode == POPUP;
    }

    /** Returns whether this is a highlighted window.
     */
    public boolean inHighlighted() {
        return _mode == HIGHLIGHTED;
    }

    /** Makes this window as a modal dialog.
     * It will automatically center the window (ignoring {@link #getLeft} and
     * {@link #getTop}).
     *
     * <p>Notice: though both setMode("modal") and doModal() both
     * causes the window to become modal, they are a bit different.
     * doModal causes the event listener to suspend immediately,
     * while setMode("modal") posts an event ({@link Events#ON_MODAL}).
     * That is, {@link #setMode} won't suspend the execution immediately,
     * but {@link #doModal} will.
     * {@link #doModal} can be called only in an event listener,
     * while {@link #setMode} can be called anytime.
     * @since 3.0.4
     */
    public void doModal() {
        if (!isEventThreadEnabled(true)) {
            checkOverlappable(MODAL_EVENT_THREAD_DISABLED);
            setNonModalMode(MODAL_EVENT_THREAD_DISABLED);
            return;
        }

        checkOverlappable(MODAL);

        if (_mode != MODAL) {
            if (!Events.inEventListener())
                throw new SuspendNotAllowedException("doModal must be called in an event listener");

            int oldmode = _mode;
            boolean oldvisi = isVisible();

            setVisible(true); //if MODAL, it must be visible; vice versa

            try {
                enterModal();
            } catch (SuspendNotAllowedException ex) {
                handleFailedModal(oldmode, oldvisi);
                throw ex;
            }
        }
    }

    private void handleFailedModal(int oldmode, boolean oldvisi) {
        try {
            if (Executions.getCurrent().getAttribute("javax.servlet.error.exception") != null) {
                //handle it specially if it is used for displaying err
                setMode(HIGHLIGHTED);
            } else {
                setMode(oldmode); //restore
                setVisible(oldvisi);
            }
        } catch (Throwable ex) {
            log.error("Causing another error", ex);
        }
    }

    /** Makes this window as overlapped with other components.
     */
    public void doOverlapped() {
        checkOverlappable(OVERLAPPED);
        setNonModalMode(OVERLAPPED);
    }

    /** Makes this window as popup, which is overlapped with other component
     * and auto-hidden when user clicks outside of the window.
     */
    public void doPopup() {
        checkOverlappable(POPUP);
        setNonModalMode(POPUP);
    }

    /** Makes this window as highlighted. The visual effect is
     * the similar to the modal window, but, like overlapped,
     * it doesn't suspend (block) the execution at the server.
     * In other words, it is more like an overlapped window from the
     * server side's viewpoint.
     */
    public void doHighlighted() {
        checkOverlappable(HIGHLIGHTED);
        setNonModalMode(HIGHLIGHTED);
    }

    /** Makes this window as embedded with other components (Default).
     */
    public void doEmbedded() {
        setNonModalMode(EMBEDDED);
    }

    /* Set non-modal mode. */
    private void setNonModalMode(int mode) {
        if (_mode != mode) {
            if (_mode == MODAL)
                leaveModal(mode);
            else {
                _mode = mode;
                smartUpdate("mode", modeToString(_mode));
            }
        }
        setVisible(true);
    }

    /** Set mode to MODAL and suspend this thread. */
    private void enterModal() {
        _mode = MODAL;
        smartUpdate("mode", modeToString(_mode));

        //no need to synchronized (_mutex) because no racing is possible
        try {
            Executions.wait(_mutex);
        } catch (InterruptedException ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    /** Resumes the suspended thread and set mode to OVERLAPPED. */
    private void leaveModal(int mode) {
        _mode = mode;
        smartUpdate("mode", modeToString(_mode));

        Executions.notifyAll(_mutex);
    }

    private boolean isEventThreadEnabled(boolean attachedRequired) {
        Desktop desktop = getDesktop();
        if (desktop == null) {
            if (attachedRequired)
                throw new SuspendNotAllowedException("Not attached, " + this);

            final Execution exec = Executions.getCurrent();
            if (exec == null || (desktop = exec.getDesktop()) == null)
                return true; //assume enabled (safer)
        }
        return desktop.getWebApp().getConfiguration().isEventThreadEnabled();
    }

    /** Makes sure it is not draggable. */
    private void checkOverlappable(int mode) {
        if (!"false".equals(getDraggable()))
            throw new UiException("Draggable window cannot be modal, overlapped, popup, or highlighted: " + this);

        if (mode == MODAL)
            for (Component comp = this; (comp = comp.getParent()) != null;)
                if (!comp.isVisible())
                    throw new UiException(
                            "One of its ancestors, " + comp + ", is not visible, so unable to be modal or highlighted");
    }

    /** Returns whether to show a close button on the title bar.
     */
    public boolean isClosable() {
        return _closable;
    }

    /** Sets whether to show a close button on the title bar.
     * If closable, a button is displayed and the onClose event is sent
     * if an user clicks the button.
     *
     * <p>Default: false.
     *
     * <p>You can intercept the default behavior by either overriding
     * {@link #onClose}, or listening the onClose event.
     *
     * <p>Note: the close button won't be displayed if no title or caption at all.
     */
    public void setClosable(boolean closable) {
        if (_closable != closable) {
            _closable = closable;
            smartUpdate("closable", closable); //re-init is required
        }
    }

    /** Returns whether the window is sizable.
     */
    public boolean isSizable() {
        return _sizable;
    }

    /** Sets whether the window is sizable.
     * If true, an user can drag the border to change the window width.
     * <p>Default: false.
     */
    public void setSizable(boolean sizable) {
        if (_sizable != sizable) {
            _sizable = sizable;
            smartUpdate("sizable", sizable);
        }
    }

    /** Returns whether to show the shadow of an overlapped/popup/modal
     * window. It is meaningless if it is an embedded window.
     * @since 3.6.0
     */
    public boolean isShadow() {
        return _shadow;
    }

    /** Sets whether to show the shadow of an overlapped/popup/modal
     * window. It is meaningless if it is an embedded window.
     * <p>Default: true.
     * @since 3.6.0
     */
    public void setShadow(boolean shadow) {
        if (_shadow != shadow) {
            _shadow = shadow;
            smartUpdate("shadow", shadow);
        }
    }

    /** Returns how to position the window at the client screen.
     * It is meaningless if the embedded mode is used.
     *
     * <p>Default: null which depends on {@link #getMode}:
     * If overlapped or popup, {@link #setLeft} and {@link #setTop} are
     * assumed. If modal or highlighted, it is centered.
     */
    public String getPosition() {
        return _pos;
    }

    /** Sets how to position the window at the client screen.
     * It is meaningless if the embedded mode is used.
     *
     * @param pos how to position. It can be null (the default), or
     * a combination of the following values (by separating with comma).
     * <dl>
     * <dt>center</dt>
     * <dd>Position the window at the center. {@link #setTop} and {@link #setLeft}
     * are both ignored.</dd>
     * <dt>nocenter</dt>
     * <dd>Not to position the window at the center. A modal window, by default,
     * will be position at the center. By specifying this value could
     * prevent it and the real position depends on {@link #setTop} and {@link #setLeft} (since 5.0.4)</dd>
     * <dt>left</dt>
     * <dd>Position the window at the left edge. {@link #setLeft} is ignored.</dd>
     * <dt>right</dt>
     * <dd>Position the window at the right edge. {@link #setLeft} is ignored.</dd>
     * <dt>top</dt>
     * <dd>Position the window at the top edge. {@link #setTop} is ignored.</dd>
     * <dt>bottom</dt>
     * <dd>Position the window at the bottom edge. {@link #setTop} is ignored.</dd>
     * <dt>parent</dt>
     * <dd>Position the window relative to its parent.
     * That is, the left and top ({@link #getTop} and {@link #getLeft})
     * is an offset to his parent's let-top corner. (since 3.0.2)</dd>
     * </dl>
     * <p>For example, "left,center" means to position it at the center of
     * the left edge.
     */
    public void setPosition(String pos) {
        //Note: we always update since the window might be dragged by an user
        _pos = pos;
        smartUpdate("position", pos);
    }

    /** Process the onClose event sent when the close button is pressed.
     * <p>Default: detach itself.
     */
    public void onClose() {
        detach();
    }

    /** Process the onModal event by making itself a modal window.
     */
    public void onModal() {
        doModal();
    }

    /** Returns the CSS style for the content block of the window.
     */
    public String getContentStyle() {
        return _cntStyle;
    }

    /** Sets the CSS style for the content block of the window.
     *
     * <p>Default: null.
     */
    public void setContentStyle(String style) {
        if (!Objects.equals(_cntStyle, style)) {
            _cntStyle = style;
            smartUpdate("contentStyle", _cntStyle);
        }
    }

    /** Returns the style class used for the content block.
     *
     * @see #setContentSclass
     */
    public String getContentSclass() {
        return _cntSclass;
    }

    /** Sets the style class used for the content block.
     *
     * @see #getContentSclass
     * @since 3.0.0
     */
    public void setContentSclass(String scls) {
        if (!Objects.equals(_cntSclass, scls)) {
            _cntSclass = scls;
            smartUpdate("contentSclass", scls);
        }
    }

    /** Makes this window as topmost.
     * It has no effect if this window is embedded.
     * @since 5.0.0
     */
    public void setTopmost() {
        smartUpdate("topmost", true);
    }

    //ZK-3678: Provide a switch to enable/disable iscroll
    /*package*/ boolean isNativeScrollbar() {
        return Utils.testAttribute(this, "org.zkoss.zul.nativebar", true, true);
    }

    // super
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);

        render(renderer, "title", _title);
        render(renderer, "maximized", _maximized);
        render(renderer, "maximizable", _maximizable);
        render(renderer, "minimized", _minimized);
        render(renderer, "minimizable", _minimizable);
        render(renderer, "closable", _closable);
        render(renderer, "sizable", _sizable);
        render(renderer, "position", _pos);
        render(renderer, "contentStyle", _cntStyle);
        render(renderer, "contentSclass", _cntSclass);
        if (_minheight != 100)
            renderer.render("minheight", _minheight);
        if (_minwidth != 200)
            renderer.render("minwidth", _minwidth);
        if (!"none".equals(_border))
            renderer.render("border", _border);
        if (!isShadow())
            renderer.render("shadow", false);
        if (_mode != EMBEDDED)
            renderer.render("mode", modeToString(_mode));
        //render mode as the last property

        //ZK-3678: Provide a switch to enable/disable iscroll
        if (!isNativeScrollbar())
            renderer.render("_nativebar", false);
    }

    public String getZclass() {
        return _zclass == null ? "z-window" : _zclass;
    }

    //-- Component --//
    public void beforeChildAdded(Component child, Component refChild) {
        if (child instanceof Caption) {
            if (_caption != null && _caption != child)
                throw new UiException("Only one caption is allowed: " + this);
        } else if (refChild instanceof Caption) {
            throw new UiException("caption must be the first child");
        }
        super.beforeChildAdded(child, refChild);
    }

    public boolean insertBefore(Component child, Component refChild) {
        if (child instanceof Caption) {
            refChild = getFirstChild();
            //always makes caption as the first child
            if (super.insertBefore(child, refChild)) {
                _caption = (Caption) child;
                return true;
            }
            return false;
        }
        return super.insertBefore(child, refChild);
    }

    public void onChildRemoved(Component child) {
        if (child instanceof Caption)
            _caption = null;
        super.onChildRemoved(child);
    }

    public void onPageDetached(Page page) {
        super.onPageDetached(page);
        if (_mode == MODAL && getPage() == null)
            leaveModal(OVERLAPPED);
    }

    /** Changes the visibility of the window.
     *
     * <p>Note if you turned on the event thread:<br/>
     * If a modal dialog becomes invisible, the modal state
     * will be ended automatically. In other words, the mode ({@link #getMode})
     * will become {@link #OVERLAPPED} and the suspending thread is resumed.
     * In other words, the modal window ({@link #MODAL}) can not be invisible
     * (while a window in other modes could be invisible).
     * <p>However, if the event thread is not enabled (default), there is no
     * such limitation. In other words, it remains the same mode when becoming
     * invisible.
     */
    public boolean setVisible(boolean visible) {
        if (visible == isVisible())
            return visible;
        _maximized = _minimized = false;
        return setVisible0(visible);
    }

    private boolean setVisible0(boolean visible) {
        if (!visible && _mode == MODAL) {
            //Hide first to avoid unpleasant effect
            super.setVisible(false);
            leaveModal(OVERLAPPED);
            return true;
        }
        return super.setVisible(visible);
    }

    //-- super --//
    public void setDraggable(String draggable) {
        if (_mode != EMBEDDED) {
            if (draggable != null && (draggable.length() > 0 && !"false".equals(draggable)))
                throw new UiException("Only embedded window could be draggable: " + this);
        }
        super.setDraggable(draggable);
    }

    //Cloneable//
    public Object clone() {
        final Window clone = (Window) super.clone();
        clone._mutex = new Mutex();
        if (clone._caption != null)
            clone.afterUnmarshal();
        return clone;
    }

    private void afterUnmarshal() {
        for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
            final Object child = it.next();
            if (child instanceof Caption) {
                _caption = (Caption) child;
                break;
            }
        }
    }

    //Serializable//
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        afterUnmarshal();
    }

    //-- ComponentCtrl --//
    /** Processes an AU request.
     *
     * <p>Default: in addition to what are handled by {@link XulElement#service},
     * it also handles onOpen.
     * @since 5.0.0
     */
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_OPEN)) {
            OpenEvent evt = OpenEvent.getOpenEvent(request);
            setVisible(evt.isOpen());
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_MAXIMIZE)) {
            MaximizeEvent evt = MaximizeEvent.getMaximizeEvent(request);
            setLeftDirectly(evt.getLeft());
            setTopDirectly(evt.getTop());
            setWidthDirectly(evt.getWidth());
            setHeightDirectly(evt.getHeight());
            _maximized = evt.isMaximized();
            if (_maximized)
                setVisibleDirectly(true);
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_MINIMIZE)) {
            MinimizeEvent evt = MinimizeEvent.getMinimizeEvent(request);
            setLeftDirectly(evt.getLeft());
            setTopDirectly(evt.getTop());
            setWidthDirectly(evt.getWidth());
            setHeightDirectly(evt.getHeight());
            _minimized = evt.isMinimized();
            if (_minimized) {
                setVisibleDirectly(false);
                if (_mode == MODAL)
                    leaveModal(OVERLAPPED);
            }
            Events.postEvent(evt);
        } else
            super.service(request, everError);
    }

    /**
     * Always return false.
     * @since 3.6.2
     */
    public boolean isCollapsible() {
        return false;
    }

    /** The window's mode used with {@link Window#setMode(Mode)}.
     * @since 6.0.0
     */
    public static enum Mode {
        /** Embeds the window as normal component. */
        EMBEDDED(Window.EMBEDDED),
        /** Makes the window as a modal dialog. once {@link #doModal}
         * is called, the execution of the event processing thread
         * is suspended until one of the following occurs.
         * <ol>
         * <li>{@link #setMode(Mode)} is called with a mode other than MODAL.</li>
         * <li>Either {@link #doOverlapped}, {@link #doPopup},
         * {@link #doEmbedded}, or {@link #doHighlighted} is called.</li>
         * <li>{@link #setVisible} is called with false.</li>
         * <li>The window is detached from the window.</li>
         * </ol>
         *
         * <p>Note: In the last two cases, the mode becomes {@link #OVERLAPPED}.
         * In other words, one might say a modal window is a special overlapped window.
         *
         * @see #HIGHLIGHTED
         */
        MODAL(Window.MODAL),
        /** Makes the window as overlapped other components.
         */
        OVERLAPPED(Window.OVERLAPPED),
        /** Makes the window as popup.
         * It is similar to {@link #OVERLAPPED}, except it is auto hidden
         * when user clicks outside of the window.
         */
        POPUP(Window.POPUP),
        /** Makes the window as highlighted.
         * Its visual effect is the same as {@link #MODAL}.
         * However, from the server side's viewpoint, it is similar to
         * {@link #OVERLAPPED}. The execution won't be suspended when
         * {@link #doHighlighted} is called.
         *
         * @see #MODAL
         * @see #OVERLAPPED
         */
        HIGHLIGHTED(Window.HIGHLIGHTED);

        private final int id;

        private Mode(int v) {
            this.id = v;
        }
    }
}