org.wings.SFrame Maven / Gradle / Ivy
/*
* Copyright 2000,2005 wingS development team.
*
* This file is part of wingS (http://wingsframework.org).
*
* wingS is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* Please see COPYING for the complete licence.
*/
package org.wings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wings.event.*;
import org.wings.header.SessionHeaders;
import org.wings.io.Device;
import org.wings.plaf.FrameCG;
import org.wings.resource.DynamicResource;
import org.wings.resource.ReloadResource;
import org.wings.session.SessionManager;
import org.wings.style.StyleSheet;
import org.wings.util.ComponentVisitor;
import org.wings.util.StringUtil;
import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.*;
/**
* The root component of every component hierarchy.
*
* A SessionServlet requires an instance of SFrame to render the page.
* SFrame consists of some header informaton (meta, link, script)
* and a stack of components. The bottommost component of the stack is always
* the contentPane. When dialogs are to be shown, they are stacked on top of
* it.
*
* @author Holger Engels,
* Armin Haaf
*/
public class SFrame
extends SRootContainer
implements PropertyChangeListener, LowLevelEventListener
{
private final transient static Logger log = LoggerFactory.getLogger(SFrame.class);
/**
* The Title of the Frame.
*/
protected String title = "Untitled";
/**
* A list of all header used by this frame.
*/
protected List headers = new ArrayList();
/**
* the style sheet used in certain look and feels.
*/
protected StyleSheet styleSheet; // IMPORTANT: initialization with null causes errors;
// These: all properties, that are installed by the plaf, are installed during the initialization of
// SComponent. The null initializations happen afterwards and overwrite the plaf installed values.
// However: null is the default initialization value, so this is not a problem!
// The same applies to all descendants of SComponent!
protected String statusLine;
/**
* The event epoch of this frame which is incremented with each invalidation.
*/
private int eventEpoch = 0;
/**
* The event epoch of this frame represented as a (preferably short) string.
*/
private String epochCache = 'W' + StringUtil.toShortestAlphaNumericString(eventEpoch);
private RequestURL requestURL = null;
private String targetResource;
private HashMap dynamicResources;
private boolean updateEnabled;
private Map updateCursor;
private Map autoAdjustLayout;
private SComponent focusComponent = null; // Component which requests the focus
/**
* @see #setBackButton(SButton)
*/
private SButton backButton;
/*
* see fireDefaultBackButton()
*/
private LastRequestListener backbuttonRequestListener;
/**
* @see #setNoCaching(boolean)
*/
private boolean noCaching = true;
/**
* For performance reasons.
*
* @see #fireInvalidLowLevelEventListener
*/
private boolean fireInvalidLowLevelEvents = false;
/**
* we need to overwrite the inherited input map from SComponent.
*/
private InputMap myInputMap;
/**
* Global input maps
*/
private HashSet globalInputMapComponents = new HashSet<>();
/**
* Should we send JS Headers in debug mode?
*/
private String logLevel = "off";
/**
* In full screen mode, the html content spans the whole size of the browser window.
*/
private boolean fullScreen;
/**
* Creates a new SFrame
*/
public SFrame() {
getSession().addPropertyChangeListener("lookAndFeel", this);
getSession().addPropertyChangeListener("request.url", this);
this.visible = false; // Frames are invisible originally.
setUpdateEnabled(true);
Map updateCursor = new HashMap<>();
SIcon cursorImage = new SResourceIcon("org/wings/icons/AjaxActivityCursor.gif");
updateCursor.put("enabled", true);
updateCursor.put("image", cursorImage.getURL().toString());
updateCursor.put("width", cursorImage.getIconWidth());
updateCursor.put("height", cursorImage.getIconHeight());
updateCursor.put("dx", 15);
updateCursor.put("dy", 0);
setUpdateCursor(updateCursor);
Map autoAdjustLayout = new HashMap<>();
autoAdjustLayout.put("enabled", true);
autoAdjustLayout.put("delay", 250);
setAutoAdjustLayout(autoAdjustLayout);
addStyle("yui-skin-sam");
}
/**
* Creates a new SFrame
*
* @param title Title of this frame, rendered in browser window title
*/
public SFrame(String title) {
this();
setTitle(title);
}
/**
* Adds a dynamic ressoure.
*
* @see #getDynamicResource(Class)
*/
public void addDynamicResource(DynamicResource d) {
if (dynamicResources == null) {
dynamicResources = new HashMap();
}
dynamicResources.put(d.getClass(), d);
}
/**
* Removes the instance of the dynamic ressource of the given class.
*
* @param dynamicResourceClass Class of dynamic ressource to remove
* @see #getDynamicResource(Class)
*/
public void removeDynamicResource(Class dynamicResourceClass) {
if (dynamicResources != null) {
dynamicResources.remove(dynamicResourceClass);
}
}
/**
* Severeral Dynamic code Ressources are attached to a SFrame
.
*
See Frame.plaf
for details, but in general you wil find attached
* to every SFrame
a
* - A {@link ReloadResource} rendering the HTML-Code of all SComponents inside this frame.
*
*/
public DynamicResource getDynamicResource(Class c) {
if (dynamicResources == null) {
dynamicResources = new HashMap();
}
return (DynamicResource) dynamicResources.get(c);
}
/**
* @return all dynamic script resources
*/
public Collection getDynamicResources() {
if (dynamicResources == null) {
dynamicResources = new HashMap();
}
return dynamicResources.values();
}
/**
* Return this
.
*
* @return this.
*/
@Override
public SFrame getParentFrame() {
return this;
}
/**
* Invalidate this frame by incrementing its event epoch. This method is called
* whenever a change took place inside this frame - that is whenever one of its
* child components has become dirty. In consequence each dynamic resource which
* is attached to this frame has to be externalized with a new "version-number".
*/
public final void invalidate() {
epochCache = "W" + (++eventEpoch);
if (isUpdatePossible() && SFrame.class.isAssignableFrom(getClass()))
update(((FrameCG) getCG()).getEpochUpdate(this, epochCache));
if (log.isDebugEnabled()) {
log.debug("Event epoch of " + this + " has been invalidated: " + epochCache);
}
}
public final String getEventEpoch() {
return epochCache;
}
/**
* Set server address.
*/
public final void setRequestURL(RequestURL requestURL) {
RequestURL oldVal = this.requestURL;
this.requestURL = requestURL;
propertyChangeSupport.firePropertyChange("requestURL", oldVal, this.requestURL);
}
/**
* Returns the base URL for a request to the WingsServlet. This URL
* is used to assemble an URL that trigger events. In order to be used
* for this purpose, you've to add your parameters here.
*/
@Override
public final RequestURL getRequestURL() {
// first time we are called, and we didn't get any change yet
if (requestURL == null) {
requestURL = (RequestURL) getSession().getProperty("request.url");
}
RequestURL result = null;
if (requestURL != null) {
result = (RequestURL) requestURL.clone();
result.setEventEpoch(epochCache);
result.setResource(getTargetResource());
}
return result;
}
/**
* Set the target resource
*/
public void setTargetResource(String targetResource) {
String oldVal = this.targetResource;
this.targetResource = targetResource;
propertyChangeSupport.firePropertyChange("targetResource", oldVal, this.targetResource);
}
/**
* Every externalized ressource has an id. A frame is a ReloadResource
.
*
* @return The id of this ReloadResource
*/
public String getTargetResource() {
if (targetResource == null) {
targetResource = getDynamicResource(ReloadResource.class).getId();
}
return targetResource;
}
/**
* Add an {@link Renderable} into the header of the HTML page
*
* @param headerElement is typically a {@link org.wings.header.Link} or {@link DynamicResource}.
* @see org.wings.header.Link
*/
public void addHeader(Object headerElement) {
if (!headers.contains(headerElement) && headerElement != null) {
headers.add(headerElement);
if (isUpdatePossible() && SFrame.class.isAssignableFrom(getClass()))
update(((FrameCG) getCG()).getAddHeaderUpdate(this, headerElement));
else
reload();
}
}
/**
* Add an {@link Renderable} into the header of the HTML page at the desired index position
*
* @param headerElement is typically a {@link org.wings.header.Link} or {@link DynamicResource}.
* @param index index in header list to add this item
* @see org.wings.header.Link
* @see #getHeaders()
*/
public void addHeader(int index, Object headerElement) {
if (!headers.contains(headerElement) && headerElement != null) {
headers.add(index, headerElement);
if (isUpdatePossible() && SFrame.class.isAssignableFrom(getClass()))
update(((FrameCG) getCG()).getAddHeaderUpdate(this, index, headerElement));
else
reload();
}
}
/**
* @see #addHeader(Object)
* @return true if this frame contained the specified header element.
*/
public boolean removeHeader(Object headerElement) {
boolean deleted = headers.remove(headerElement);
if (deleted) {
if (isUpdatePossible() && SFrame.class.isAssignableFrom(getClass()))
update(((FrameCG) getCG()).getRemoveHeaderUpdate(this, headerElement));
else
reload();
}
return deleted;
}
/**
* Removes all headers. Be carful about what you do!
*
* @see #addHeader(Object)
*/
public void clearHeaders() {
headers.clear();
reload();
}
/**
* @see #addHeader(Object)
* @deprecated Use {@link #getHeaders()} instead
*/
public List headers() {
return getHeaders();
}
/**
* @see #addHeader(Object)
*/
public List getHeaders() {
return Collections.unmodifiableList(headers);
}
/**
* Sets the title of this HTML page. Typically shown in the browsers window title.
*
* @param title The window title.
*/
public void setTitle(String title) {
String oldVal = this.title;
if ( title == null ) {
title = "";
}
if ( !title.equals(this.title) ) {
this.title = title;
reload();
}
propertyChangeSupport.firePropertyChange("title", oldVal, this.title);
}
/**
* Title of this HTML page. Typically shown in the browsers window title.
*
* @return the current page title or an empty string if this page doesn't have a title.
*/
public String getTitle() {
return title;
}
public void setStatusLine(String s) {
String oldVal = this.statusLine;
statusLine = s;
propertyChangeSupport.firePropertyChange("statusLine", oldVal, this.statusLine);
}
/**
* @return true
if the generated HTML code of this frame/page should
* not be cached by browser, false
if no according HTTP/HTML headers
* should be rendered
* @see #setNoCaching(boolean)
*/
public boolean isNoCaching() {
return noCaching;
}
@Override
public void write(Device s) throws IOException {
if (noCaching) {
reload(); // invalidate frame on each rendering!
}
super.write(s);
}
/**
* Typically you don't want any wings application to operate on old 'views' meaning
* old pages. Hence all generated HTML pages (SFrame
objects
* rendered through {@link ReloadResource} are marked as do not cache
* inside the HTTP response header and the generated HTML frame code.
* If for any purpose (i.e. you a writing a read only application) you want
* th user to be able to work on old views then set this to false
* and Mark the according SComponent
s to be not epoch checked
* (i.e. {@link SAbstractButton#setEpochCheckEnabled(boolean)})
*
* @param noCaching The noCaching to set.
* @see LowLevelEventListener#isEpochCheckEnabled()
* @see org.wings.session.LowLevelEventDispatcher
*/
public void setNoCaching(boolean noCaching) {
boolean oldVal = this.noCaching;
this.noCaching = noCaching;
propertyChangeSupport.firePropertyChange("noCaching", oldVal, this.noCaching);
}
/**
* Shows this frame. This means it gets registered at the session.
*
* @see org.wings.session.Session#getFrames()
*/
public void show() {
setVisible(true);
}
/**
* Hides this frame. This means it gets removed at the session.
*
* @see org.wings.session.Session#getFrames()
*/
public void hide() {
setVisible(false);
}
/**
* Shows or hide this frame. This means it gets (un)registered at the session.
*
* @see org.wings.session.Session#getFrames()
*/
@Override
public void setVisible(boolean visible) {
if (visible != isVisible()) {
if (visible) {
List newHeaders = new ArrayList(SessionHeaders.getInstance().getHeaders());
for (Object oldHeaders : headers) {
if (!newHeaders.contains(oldHeaders)) {
newHeaders.add(oldHeaders);
}
}
headers = newHeaders;
getSession().addFrame(this);
register();
} else {
getSession().removeFrame(this);
unregister();
}
super.setVisible(visible);
setRecursivelyVisible(visible);
}
}
@Override
public void propertyChange(PropertyChangeEvent pe) {
if ("lookAndFeel".equals(pe.getPropertyName())) {
updateComponentTreeCG(getContentPane());
}
if ("request.url".equals(pe.getPropertyName())) {
setRequestURL((RequestURL) pe.getNewValue());
}
}
private void updateComponentTreeCG(SComponent c) {
c.updateCG();
if (c instanceof SContainer) {
SComponent[] children = ((SContainer) c).getComponents();
for (SComponent aChildren : children) {
updateComponentTreeCG(aChildren);
}
}
updateCG();
}
public void setCG(FrameCG cg) {
super.setCG(cg);
}
@Override
public void invite(ComponentVisitor visitor)
throws Exception {
visitor.visit(this);
}
/**
* Choose which component rendered inside this frame should gain the edit focus on next rendering
* This function is called by {@link SComponent#requestFocus()}
*
* @param focusOnComponent the component which requests the focus.
*/
public void setFocus(SComponent focusOnComponent) {
focusComponent = focusOnComponent;
if (focusComponent != null && isUpdatePossible())
update(((FrameCG) getCG()).getFocusUpdate(this, focusComponent));
}
/**
* @see #setFocus(SComponent)
*/
public SComponent getFocus() {
return focusComponent;
}
@Override
public void processLowLevelEvent(String name, String... values) {
focusComponent = null;
if (values.length == 1 && name.endsWith("_focus")) {
String eventId = values[0];
List listeners = getSession().getDispatcher().getLowLevelEventListener(eventId);
for (int i = 0; i < listeners.size() && focusComponent == null; i++) {
Object listener = listeners.get(i);
if (listener instanceof SComponent) {
this.focusComponent = (SComponent)listener;
}
}
}
/*
* When there is a debug Cookie,
* change the debug headers in the CG according to the value of the
* cookie.
*/
if (name.endsWith("_debug")) {
log.info("input "+name+ Arrays.toString(values));
String newLogLevel = (values.length == 1)
? values[0] != null
? values[0]
: "off"
: "off";
logLevel = newLogLevel;
}
}
/**
* Registers an {@link SInvalidLowLevelEventListener} in this frame.
*
* @param l The listener to notify about outdated reqests
* @see org.wings.event.InvalidLowLevelEvent
*/
public final void addInvalidLowLevelEventListener(SInvalidLowLevelEventListener l) {
addEventListener(SInvalidLowLevelEventListener.class, l);
fireInvalidLowLevelEvents = true;
}
/**
* Removes the passed {@link SInvalidLowLevelEventListener} from this frame.
*
* @param l The listener to remove
* @see org.wings.event.InvalidLowLevelEvent
*/
public final void removeDispatchListener(SInvalidLowLevelEventListener l) {
removeEventListener(SInvalidLowLevelEventListener.class, l);
}
/**
* Notify all {@link SInvalidLowLevelEventListener} about an outdated request
* on the passed component
*
* @param source The SComponent
received an outdated event
* @see org.wings.session.LowLevelEventDispatcher
*/
public final void fireInvalidLowLevelEventListener(LowLevelEventListener source) {
if (fireInvalidLowLevelEvents) {
Object[] listeners = getListenerList();
InvalidLowLevelEvent e = null;
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SInvalidLowLevelEventListener.class) {
// Lazily create the event:
if (e == null) {
e = new InvalidLowLevelEvent(source);
}
((SInvalidLowLevelEventListener) listeners[i + 1]).invalidLowLevelEvent(e);
}
}
}
fireDefaultBackButton();
}
/**
* A button activated on detected browser back clicks.
*
* @return Returns the backButton.
* @see #setBackButton(SButton)
*/
public SButton getBackButton() {
return backButton;
}
/**
* This button allows you to programattically react on Back buttons pressed in the browser itselfs.
* This is a convenience method in contrast to {@link #addInvalidLowLevelEventListener(SInvalidLowLevelEventListener)}.
* While the listener throws an event on every detected component receiving an invalid
* request, this button is only activated if
*
* - Maximum once per request
*
- Only if some time passed by to avoid double-clicks to be recognized as back button clicks.
*
* Note: To work correctly you should set use GET posting
* {@link SForm#setPostMethod(boolean)} and use {@link SFrame#setNoCaching(boolean)} for
* no caching. This will advise the browser to reload every back page.
*
* @param defaultBackButton A button to trigger upon detected invalid epochs.
*/
public void setBackButton(SButton defaultBackButton) {
if (backbuttonRequestListener == null) {
backbuttonRequestListener = new LastRequestListener();
getSession().addRequestListener(backbuttonRequestListener);
}
this.backButton = defaultBackButton;
}
/**
* Fire back button only once and if some time already passed by to avoid double clicks.
*/
private void fireDefaultBackButton() {
if (this.backButton != null && this.backbuttonRequestListener != null) {
long currentTime = System.currentTimeMillis();
final int DISPATCHINGFINISH_BACKBUTTON_DEADTIME = 500;
if (currentTime - backbuttonRequestListener.lastDispatchingFinished > DISPATCHINGFINISH_BACKBUTTON_DEADTIME) {
// Simulate a button press
backButton.processLowLevelEvent(null, "1");
}
}
}
@Override
public void fireIntermediateEvents() {
}
/**
* @see LowLevelEventListener#isEpochCheckEnabled()
*/
private boolean epochCheckEnabled = true;
/**
* @see LowLevelEventListener#isEpochCheckEnabled()
*/
@Override
public boolean isEpochCheckEnabled() {
return epochCheckEnabled;
}
/**
* @see LowLevelEventListener#isEpochCheckEnabled()
*/
public void setEpochCheckEnabled(boolean epochCheckEnabled) {
this.epochCheckEnabled = epochCheckEnabled;
}
/**
* custom error handling. If you want to catch application errors,
* return true here.
* @param e The throwable causing this.
* @return does this frame handle errors...
*/
public static boolean handleError(Throwable e) {
return false;
}
@Override
public InputMap getInputMap(int condition) {
// SFrame has only one inputMap
if (myInputMap == null) {
myInputMap = new InputMap();
}
return myInputMap;
}
@Override
public void setInputMap(int condition, InputMap inputMap) {
this.myInputMap = inputMap;
}
public void registerGlobalInputMapComponent(SComponent comp) {
if (!globalInputMapComponents.contains(comp)) {
// not yet registered
globalInputMapComponents.add(comp);
}
}
public void deregisterGlobalInputMapComponent(SComponent comp) {
if (globalInputMapComponents != null) {
globalInputMapComponents.remove(comp);
}
}
public Set getGlobalInputMapComponents() {
return globalInputMapComponents;
}
/**
* Private helper class to remember the end of the last dispatch.
* @see org.wings.SFrame#fireDefaultBackButton()
*/
private static class LastRequestListener implements SRequestListener {
private long lastDispatchingFinished = -1;
@Override
public void processRequest(SRequestEvent e) {
if (e.getType() == SRequestEvent.DISPATCH_DONE)
lastDispatchingFinished = System.currentTimeMillis();
}
}
public boolean isUpdateEnabled() {
return updateEnabled;
}
public void setUpdateEnabled(boolean enabled) {
if (updateEnabled != enabled) {
if (isUpdatePossible() && SFrame.class.isAssignableFrom(getClass()))
update(((FrameCG) getCG()).getUpdateEnabledUpdate(this, enabled));
else
reload();
updateEnabled = enabled;
}
}
public Map getUpdateCursor() {
return updateCursor;
}
public void setUpdateCursor(Map updateCursor) {
if (isDifferent(this.updateCursor, updateCursor)) {
Map oldVal = this.updateCursor;
this.updateCursor = updateCursor;
reload();
propertyChangeSupport.firePropertyChange("updateCursor", oldVal, this.updateCursor);
}
}
public boolean isFullScreen() {
return fullScreen;
}
public void setFullScreen(boolean fullScreen) {
if (isDifferent(this.fullScreen, fullScreen)) {
boolean oldFullScreen = this.fullScreen;
this.fullScreen = fullScreen;
if (fullScreen)
getContentPane().setPreferredSize(SDimension.FULLAREA);
reload();
propertyChangeSupport.firePropertyChange("fullScreen", oldFullScreen, this.fullScreen);
}
}
public Map getAutoAdjustLayout() {
return autoAdjustLayout;
}
public void setAutoAdjustLayout(Map autoAdjustLayout) {
if (isDifferent(this.autoAdjustLayout, autoAdjustLayout)) {
this.autoAdjustLayout = autoAdjustLayout;
reload();
}
}
@Override
protected void initializeContentPane() {
setContentPane(new SForm(new SBorderLayout()));
}
/**
* Tell wether the contentPane is an SForm.
* @return true
if the contentPane is an SForm, false
otherwise
*/
public boolean isFormContentPane() {
return contentPane instanceof SForm;
}
/**
* Determine wether the contentPane shall be an SForm. The property is true by default.
* @param contentPaneForm true
if the contentPane shall be an SForm, false
otherwise
*/
public void setFormContentPane(boolean contentPaneForm) {
if (contentPane instanceof SForm && !contentPaneForm) {
SPanel newPanel = new SPanel();
rebuildPanel(contentPane, newPanel);
setContentPane(newPanel);
}
else if (!(contentPane instanceof SForm) && contentPaneForm) {
SForm newPanel = new SForm();
rebuildPanel(contentPane, newPanel);
setContentPane(newPanel);
}
}
private static void rebuildPanel(SContainer oldPanel, SContainer newPanel) {
SLayoutManager layoutManager = oldPanel.getLayout();
SComponent[] components = oldPanel.getComponents();
ArrayList constraints = oldPanel.getConstraintList();
oldPanel.removeAll();
oldPanel.setLayout(null);
newPanel.setLayout(layoutManager);
for (int i = 0; i < components.length; i++) {
SComponent component = components[i];
Object constraint = constraints.get(i);
newPanel.add(component, constraint);
}
}
public static String getLogLevel() {
String[] debugSettings = (String[])SessionManager.getSession().getProperty("debug.cookie");
if (debugSettings != null) {
for (String debugSetting : debugSettings) {
if (debugSetting != null && debugSetting.startsWith("loglevel=")) {
return debugSetting.substring(9);
}
}
}
return null;
}
public static boolean isDebugJs() {
String[] debugSettings = (String[])SessionManager.getSession().getProperty("debug.cookie");
if (debugSettings != null) {
for (String debugSetting : debugSettings) {
if ("javascript".equals(debugSetting)) {
return true;
}
}
}
return false;
}
}