/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.server.headlessclient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.behavior.IBehavior;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
import org.apache.wicket.model.Model;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.IBasicFormManager;
import com.servoy.j2db.MediaURLStreamHandler;
import com.servoy.j2db.server.headlessclient.dataui.AbstractServoyDefaultAjaxBehavior;
import com.servoy.j2db.server.headlessclient.dataui.ChangesRecorder;
import com.servoy.j2db.server.headlessclient.dataui.ISupportWebTabSeq;
import com.servoy.j2db.server.headlessclient.dataui.IWebFormContainer;
import com.servoy.j2db.server.headlessclient.dataui.StripHTMLTagsConverter;
import com.servoy.j2db.server.headlessclient.dataui.WebDataHtmlArea;
import com.servoy.j2db.server.headlessclient.dataui.WebEventExecutor;
import com.servoy.j2db.server.headlessclient.dataui.WebSplitPane;
import com.servoy.j2db.server.headlessclient.eventthread.IEventDispatcher;
import com.servoy.j2db.server.headlessclient.eventthread.WicketEvent;
import com.servoy.j2db.ui.IComponent;
import com.servoy.j2db.ui.IProviderStylePropertyChanges;
import com.servoy.j2db.ui.IStylePropertyChanges;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.OrientationApplier;
import com.servoy.j2db.util.Pair;
import com.servoy.j2db.util.Utils;
/**
* Implementation of {@link IPageContributorInternal} that is a wicket component that is added to the page for adding all kinds of behaviors and scripts to the main page.
*
* @author jcompagner
*/
public class PageContributor extends WebMarkupContainer implements IPageContributorInternal
{
private static final long serialVersionUID = 1L;
public static final ResourceReference anchorlayout = new JavascriptResourceReference(PageContributor.class, "anchorlayout.js"); //$NON-NLS-1$
private IRepeatingView repeatingView;
private final Map<String, IBehavior> behaviors = new HashMap<String, IBehavior>();
private final EventCallbackBehavior eventCallbackBehavior;
private StringBuffer dynamicJS;
protected ChangesRecorder jsChangeRecorder = new ChangesRecorder(null, null);
private long lastTableUpdate = -1;
private final List<Component> tablesToRender = new ArrayList<Component>();
private SortedSet<FormAnchorInfo> formAnchorInfos;
private final Map<String, Integer> tabIndexChanges = new HashMap<String, Integer>();
private boolean anchorInfoChanged = false;
private StringBuffer componentsThatNeedAnchorRelayout;
private boolean isResizing = false;
private final IApplication application;
private final ArrayList<WebSplitPane> splitPanesToUpdateDivider = new ArrayList<WebSplitPane>();
public PageContributor(final IApplication application, String id)
{
super(id, new Model());
this.application = application;
setOutputMarkupPlaceholderTag(true);
add(new AbstractServoyDefaultAjaxBehavior()
{
private static final long serialVersionUID = 1L;
@Override
protected void respond(AjaxRequestTarget target)
{
String update = getRequestCycle().getRequest().getParameter("update"); //$NON-NLS-1$
// get the update parameter and check if that is still the same, else wait for the next.
if (Long.parseLong(update) == lastTableUpdate)
{
for (int i = 0; i < tablesToRender.size(); i++)
{
Component comp = tablesToRender.get(i);
if (comp.isVisibleInHierarchy())
{
target.addComponent(comp);
}
}
tablesToRender.clear();
WebEventExecutor.generateResponse(target, findPage());
}
else
{
Debug.log("IGNORED TABLE REQUEST");
}
}
@Override
public void renderHead(IHeaderResponse response)
{
super.renderHead(response);
response.renderOnDomReadyJavascript(getCallbackScript().toString());
}
@Override
public CharSequence getCallbackUrl(boolean onlyTargetActivePage)
{
CharSequence url = super.getCallbackUrl(true);
url = url + "&update=" + lastTableUpdate; //$NON-NLS-1$
return url;
}
@Override
public boolean isEnabled(Component component)
{
return tablesToRender.size() > 0 && super.isEnabled(component);
}
});
add(eventCallbackBehavior = new EventCallbackBehavior());
add(new AbstractServoyDefaultAjaxBehavior()
{
@Override
public void renderHead(IHeaderResponse response)
{
if (isFormWidthZero())
{
response.renderOnLoadJavascript("Servoy.Resize.onWindowResize();"); //$NON-NLS-1$
}
}
@Override
protected void respond(AjaxRequestTarget target)
{
// not used
}
private boolean isFormWidthZero()
{
final boolean[] returnValue = { false };
Page page = findPage();
if (page != null)
{
page.visitChildren(WebForm.class, new Component.IVisitor<WebForm>()
{
public Object component(WebForm form)
{
if (form.getFormWidth() == 0 && form.isVisibleInHierarchy())
{
IWebFormContainer formContainer = form.findParent(IWebFormContainer.class);
if (!(formContainer instanceof WebSplitPane))
{
returnValue[0] = true;
return IVisitor.STOP_TRAVERSAL;
}
}
return IVisitor.CONTINUE_TRAVERSAL;
}
});
}
return returnValue[0];
}
});
}
@Override
public void renderHead(HtmlHeaderContainer container)
{
super.renderHead(container);
IHeaderResponse response = container.getHeaderResponse();
String djs = getDynamicJavaScript();
if (djs != null)
{
response.renderOnLoadJavascript(djs);
}
addReferences(response);
Page page = findPage();
if (page instanceof MainPage)
{
Component focus = ((MainPage)page).getAndResetToFocusComponent();
if (focus != null)
{
if (focus instanceof WebDataHtmlArea)
{
response.renderOnLoadJavascript("tinyMCE.activeEditor.focus()");
}
else
{
response.renderOnLoadJavascript("setTimeout(\"requestFocus('" + focus.getMarkupId() + "');\",0);"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
if (formAnchorInfos != null && formAnchorInfos.size() != 0 && WebClientSession.get() != null && WebClientSession.get().getWebClient() != null &&
Utils.getAsBoolean(WebClientSession.get().getWebClient().getRuntimeProperties().get("enableAnchors"))) //$NON-NLS-1$
{
if (anchorInfoChanged)
{
response.renderJavascriptReference(anchorlayout);
response.renderOnLoadJavascript("setTimeout(\"layoutEntirePage();\", 10);"); // setTimeout is important here, to let the browser apply CSS styles during Ajax calls //$NON-NLS-1$
String sb = FormAnchorInfo.generateAnchoringFunctions(formAnchorInfos, getOrientation());
response.renderJavascript(sb, null);
anchorInfoChanged = false;
}
else if (componentsThatNeedAnchorRelayout != null && componentsThatNeedAnchorRelayout.length() > 0)
{
response.renderJavascriptReference(anchorlayout);
response.renderOnLoadJavascript("setTimeout(\"layoutSpecificElements();\", 10);");
response.renderJavascript("executeLayoutSpecificElements = function()\n{\n" + componentsThatNeedAnchorRelayout.append("\n}"), null);
}
}
if (componentsThatNeedAnchorRelayout != null) componentsThatNeedAnchorRelayout.setLength(0);
if (tabIndexChanges.size() > 0)
{
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("Servoy.TabCycleHandling.setNewTabIndexes(["); //$NON-NLS-1$
for (String componentID : tabIndexChanges.keySet())
{
stringBuffer.append("['");//$NON-NLS-1$
stringBuffer.append(componentID);
stringBuffer.append("',");//$NON-NLS-1$
stringBuffer.append(tabIndexChanges.get(componentID));
stringBuffer.append("]");//$NON-NLS-1$
stringBuffer.append(",");//$NON-NLS-1$
}
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
stringBuffer.append("]);"); //$NON-NLS-1$
response.renderOnLoadJavascript(stringBuffer.toString());
tabIndexChanges.clear();
}
if (splitPanesToUpdateDivider.size() > 0)
{
for (WebSplitPane splitPane : splitPanesToUpdateDivider)
{
if (splitPane.findParent(Page.class) != null && !splitPane.getScriptObject().getChangesRecorder().isChanged() &&
!splitPane.isParentContainerChanged())
{
response.renderOnLoadJavascript((new StringBuilder("(function() {").append(splitPane.getDividerLocationJSSetter(true).append("}).call();"))).toString()); //$NON-NLS-1$ //$NON-NLS-2$
}
}
splitPanesToUpdateDivider.clear();
}
// Enable this for Firebug debugging under IE/Safari/etc.
//response.renderJavascriptReference("http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js"); //$NON-NLS-1$
}
private void addReferences(IHeaderResponse response)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
List<Pair<Byte, Object>> resources = grr.getAllResources();
for (Pair<Byte, Object> resource : resources)
{
String url = null;
if (resource.getRight() instanceof ResourceReference)
{
url = RequestCycle.get().urlFor((ResourceReference)resource.getRight()).toString();
}
else if (resource.getRight() instanceof String)
{
url = (String)resource.getRight();
if (url.contains(MediaURLStreamHandler.MEDIA_URL_DEF))
{
url = StripHTMLTagsConverter.convertMediaReferences(url, application.getSolution().getName(), new ResourceReference("media"), "", true).toString(); //$NON-NLS-1$//$NON-NLS-2$
}
}
if (url != null)
{
if (ResourceReferences.JS.equals(resource.getLeft()))
{
response.renderJavascriptReference(url);
}
else if (ResourceReferences.CSS.equals(resource.getLeft()))
{
response.renderCSSReference(url);
}
}
}
}
}
private String getOrientation()
{
String orientation = OrientationApplier.getHTMLContainerOrientation(application.getLocale(), application.getSolution().getTextOrientation());
if (orientation.equals(AttributeModifier.VALUELESS_ATTRIBUTE_REMOVE)) orientation = "ltr"; //$NON-NLS-1$
return orientation;
}
public void removeFormAnchorInfo(FormAnchorInfo fai)
{
if (formAnchorInfos != null) formAnchorInfos.remove(fai);
}
public void setFormAnchorInfos(SortedSet<FormAnchorInfo> infos)
{
anchorInfoChanged = !Utils.equalObjects(formAnchorInfos, infos);
if (infos == null)
{
formAnchorInfos = null;
}
else
{
if (anchorInfoChanged)
{
if (!isResizing) getStylePropertyChanges().setChanged();
formAnchorInfos = infos;
}
}
}
public void markComponentForAnchorLayoutIfNeeded(Component component)
{
if (formAnchorInfos != null && formAnchorInfos.size() != 0)
{
// see if this component is actually affected by layout or not and generate anchoring properties for it if it is
String s = FormAnchorInfo.generateAnchoringParams(formAnchorInfos, component);
if (s != null)
{
if (componentsThatNeedAnchorRelayout == null) componentsThatNeedAnchorRelayout = new StringBuffer();
componentsThatNeedAnchorRelayout.append("layoutOneElement(").append(s).append(");\n");
getStylePropertyChanges().setChanged();
}
}
}
public void setResizing(boolean b)
{
isResizing = b;
}
public void addTableToRender(Component comp)
{
getStylePropertyChanges().setChanged();
if (!tablesToRender.contains(comp)) tablesToRender.add(comp);
lastTableUpdate = System.currentTimeMillis();
}
public void addTabIndexChange(String componentID, int tabIndex)
{
if (tabIndex != ISupportWebTabSeq.DEFAULT)
{
tabIndexChanges.put(componentID, Integer.valueOf(tabIndex));
}
else
{
tabIndexChanges.remove(componentID);
}
}
public void addBehavior(String name, IBehavior behavior)
{
if (behaviors.put(name, behavior) == null)
{
getStylePropertyChanges().setChanged();
add(behavior);
}
}
public void removeBehavior(String name)
{
IBehavior behavior = null;
if ((behavior = behaviors.remove(name)) != null)
{
getStylePropertyChanges().setChanged();
if (RequestCycle.get() != null)
{
remove(behavior);
}
}
}
public void addDynamicJavaScript(String js)
{
if (dynamicJS == null) dynamicJS = new StringBuffer();
dynamicJS.append(js);
getStylePropertyChanges().setChanged();
}
private String getDynamicJavaScript()
{
String retval = null;
if (dynamicJS != null) retval = dynamicJS.toString();
dynamicJS = null;
return retval;
}
protected ResourceReferences getGlobalResourceReferences()
{
ResourceReferences grr = null;
IBasicFormManager fm = application.getFormManager();
if (fm instanceof WebFormManager)
{
grr = ((WebFormManager)fm).getGlobalResourceReferences();
}
return grr;
}
@Override
public void addGlobalCSSResourceReference(ResourceReference resource)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.addGlobalCSSResourceReference(resource);
getStylePropertyChanges().setChanged();
}
}
@Override
public void addGlobalJSResourceReference(String url)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.addGlobalJSResourceReference(url);
getStylePropertyChanges().setChanged();
}
}
@Override
public void addGlobalJSResourceReference(ResourceReference resource)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.addGlobalJSResourceReference(resource);
getStylePropertyChanges().setChanged();
}
}
@Override
public void addGlobalCSSResourceReference(String url)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.addGlobalCSSResourceReference(url);
getStylePropertyChanges().setChanged();
}
}
@Override
public void removeGlobalResourceReference(ResourceReference resource)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.removeGlobalResourceReference(resource);
getStylePropertyChanges().setChanged();
}
}
@Override
public void removeGlobalResourceReference(String url)
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
grr.removeGlobalResourceReference(url);
getStylePropertyChanges().setChanged();
}
}
@Override
public List<Object> getGlobalCSSResources()
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
return grr.getGlobalCSSResources();
}
return Collections.emptyList();
}
@Override
public List<Object> getGlobalJSResources()
{
ResourceReferences grr = getGlobalResourceReferences();
if (grr != null)
{
return grr.getGlobalJSResources();
}
return Collections.emptyList();
}
public IBehavior getBehavior(String name)
{
return behaviors.get(name);
}
public IStylePropertyChanges getStylePropertyChanges()
{
return jsChangeRecorder;
}
@Override
protected void onRender(MarkupStream markupStream)
{
super.onRender(markupStream);
getStylePropertyChanges().setRendered();
}
public static List<Component> getVisibleChildren(Component component, final boolean onlyChanged)
{
final List<Component> visibleChildren = new ArrayList<Component>();
if (component.isVisibleInHierarchy() &&
(!onlyChanged || (component instanceof IProviderStylePropertyChanges && ((IProviderStylePropertyChanges)component).getStylePropertyChanges().isChanged())))
{
visibleChildren.add(component);
}
if (component instanceof MarkupContainer)
{
((MarkupContainer)component).visitChildren(IProviderStylePropertyChanges.class, new IVisitor<Component>()
{
public Object component(Component stylePropertyChange)
{
if (!stylePropertyChange.isVisibleInHierarchy())
{
return IVisitor.CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
}
if (onlyChanged && !((IProviderStylePropertyChanges)stylePropertyChange).getStylePropertyChanges().isChanged())
{
return IVisitor.CONTINUE_TRAVERSAL;
}
visibleChildren.add(stylePropertyChange);
// add all children from here
if (stylePropertyChange instanceof MarkupContainer)
{
((MarkupContainer)stylePropertyChange).visitChildren(IComponent.class, new IVisitor<Component>()
{
public Object component(Component fieldComponent)
{
if (!fieldComponent.isVisibleInHierarchy())
{
return IVisitor.CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
}
visibleChildren.add(fieldComponent);
return IVisitor.CONTINUE_TRAVERSAL;
}
});
}
return IVisitor.CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
}
});
}
return visibleChildren;
}
private class EventCallbackBehavior extends AbstractServoyDefaultAjaxBehavior
{
private static final long serialVersionUID = 1L;
@Override
protected void respond(final AjaxRequestTarget target)
{
if (Debug.tracing()) Debug.trace("Event response callback " + getRequestCycle().getRequest().getURL()); //$NON-NLS-1$
final String markupId = getRequestCycle().getRequest().getParameter("id"); //$NON-NLS-1$
final String event = getRequestCycle().getRequest().getParameter("event"); //$NON-NLS-1$
if (markupId != null && event != null)
{
final MainPage callback = findParent(MainPage.class);
if (callback == null)
{
Debug.trace("Callback handler not found, event=" + event + " id=" + markupId); //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
IEventDispatcher<WicketEvent> eventDispatcher = ((WebClient)application).getEventDispatcher();
if (eventDispatcher != null)
{
eventDispatcher.addEvent(new WicketEvent((WebClient)application, new Runnable()
{
public void run()
{
callback.respond(target, event, markupId);
}
}));
WebEventExecutor.generateResponse(target, getPage());
}
else
{
callback.respond(target, event, markupId);
}
}
}
else
{
Debug.error("Missing id or event parameter in callback " + getRequestCycle().getRequest().getURL()); //$NON-NLS-1$
}
}
@Override
public CharSequence getCallbackUrl(boolean onlyTargetActivePage)
{
return super.getCallbackUrl(true);
}
}
/**
* @param container
*/
public void addRepeatingView(IRepeatingView rp)
{
this.repeatingView = rp;
}
/*
* (non-Javadoc)
*
* @see com.servoy.j2db.server.headlessclient.IPageContributor#getRepeatingView()
*/
public IRepeatingView getRepeatingView()
{
return repeatingView;
}
/*
* (non-Javadoc)
*
* @see com.servoy.j2db.server.headlessclient.IPageContributorInternal#getEventCallback()
*/
public AbstractAjaxBehavior getEventCallback()
{
return eventCallbackBehavior;
}
public void addSplitPaneToUpdatedDivider(WebSplitPane splitPane)
{
if (splitPanesToUpdateDivider.indexOf(splitPane) == -1) splitPanesToUpdateDivider.add(splitPane);
getStylePropertyChanges().setChanged();
}
}