/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2013 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.ngclient.component; import java.awt.Point; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.mozilla.javascript.Function; import org.mozilla.javascript.Wrapper; import org.sablo.WebComponent; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectApiDefinition; import org.sablo.specification.WebObjectSpecification; import org.sablo.websocket.CurrentWindow; import org.sablo.websocket.IWindow; import com.servoy.base.persistence.constants.IFormConstants; import com.servoy.j2db.BasicFormController; import com.servoy.j2db.DesignModeCallbacks; import com.servoy.j2db.IBasicFormManager; import com.servoy.j2db.IFormController; import com.servoy.j2db.IView; import com.servoy.j2db.dataprocessing.IRecordInternal; import com.servoy.j2db.dataprocessing.PrototypeState; import com.servoy.j2db.persistence.Form; import com.servoy.j2db.persistence.IFormElement; import com.servoy.j2db.persistence.Part; import com.servoy.j2db.persistence.PositionComparator; import com.servoy.j2db.persistence.StaticContentSpecLoader; import com.servoy.j2db.scripting.DefaultScope; import com.servoy.j2db.scripting.JSApplication.FormAndComponent; import com.servoy.j2db.scripting.JSEvent; import com.servoy.j2db.server.ngclient.FormElement; import com.servoy.j2db.server.ngclient.FormHTMLAndJSGenerator; import com.servoy.j2db.server.ngclient.IDataAdapterList; import com.servoy.j2db.server.ngclient.INGApplication; import com.servoy.j2db.server.ngclient.INGClientWindow; import com.servoy.j2db.server.ngclient.IWebFormController; import com.servoy.j2db.server.ngclient.IWebFormUI; import com.servoy.j2db.server.ngclient.NGClientWindow; import com.servoy.j2db.server.ngclient.NGRuntimeWindow; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.WebFormUI; import com.servoy.j2db.server.ngclient.WebListFormUI; import com.servoy.j2db.server.ngclient.eventthread.NGClientWebsocketSessionWindows; import com.servoy.j2db.server.ngclient.property.types.NGTabSeqPropertyType; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.SortedList; import com.servoy.j2db.util.Utils; /** * @author lvostinar * */ public class WebFormController extends BasicFormController implements IWebFormController { private int view = -1; private WebFormUI formUI; private boolean rendering; private String[] tabSequence; public WebFormController(INGApplication application, Form form, String name) { super(application, form, name); initFormUI(); } public void initFormUI() { Object parentContainer = null; if (formUI != null) { parentContainer = formUI.getParentContainer(); } switch (form.getView()) { case IFormConstants.VIEW_TYPE_TABLE : case IFormConstants.VIEW_TYPE_TABLE_LOCKED : case IFormConstants.VIEW_TYPE_LIST : case IFormConstants.VIEW_TYPE_LIST_LOCKED : formUI = new WebListFormUI(this); break; default : formUI = new WebFormUI(this); } if (parentContainer instanceof String) { formUI.setParentWindowName((String)parentContainer); } else if (parentContainer instanceof WebFormComponent) { formUI.setParentContainer((WebFormComponent)parentContainer); } } @Override public final INGApplication getApplication() { return (INGApplication)super.getApplication(); } @Override public IWebFormUI getFormUI() { return formUI; } @Override public void setView(int view) { if (view == -1) this.view = form.getView(); else this.view = view; } @Override public int getView() { return view; } @Override public IBasicFormManager getBasicFormManager() { return getApplication().getFormManager(); } @Override protected IView getViewComponent() { return formUI; } @Override public void showNavigator(List<Runnable> invokeLaterRunnables) { String parentWindowName = getFormUI().getParentWindowName(); NGRuntimeWindow window = getApplication().getRuntimeWindowManager().getWindow(parentWindowName); if (window != null && window.getController() == this) { IFormController currentNavigator = window.getNavigator(); int form_id = form.getNavigatorID(); if (form_id > 0) { if (currentNavigator == null || currentNavigator.getForm().getID() != form_id)//is already there { if (currentNavigator != null) { currentNavigator.notifyVisible(false, invokeLaterRunnables); } Form navigator = application.getFlattenedSolution().getForm(form_id); if (navigator != null) { IFormController navigatorController = getApplication().getFormManager().getForm(navigator.getName()); navigatorController.notifyVisible(true, invokeLaterRunnables); } } else { // Try to lease it extra so it will be added to last used screens. Form navigator = application.getFlattenedSolution().getForm(form_id); if (navigator != null) { getBasicFormManager().leaseFormPanel(navigator.getName()); } } } else if (form_id != Form.NAVIGATOR_IGNORE) { if (currentNavigator != null) currentNavigator.notifyVisible(false, invokeLaterRunnables); } window.setNavigator(form_id); } } @Override public boolean stopUIEditing(boolean looseFocus) { if (isDestroyed()) return true; if (!getFormUI().getDataAdapterList().stopUIEditing(looseFocus)) return false; if (looseFocus && form.getOnRecordEditStopMethodID() != 0) { //allow beans to store there data via method IRecordInternal[] records = getApplication().getFoundSetManager().getEditRecordList().getUnmarkedEditedRecords(formModel); for (IRecordInternal element : records) { boolean b = executeOnRecordEditStop(element); if (!b) return false; } } return true; } public void setRendering(boolean rendering) { if (rendering == this.rendering) throw new IllegalArgumentException("rendering is already: " + this.rendering); this.rendering = rendering; } @Override public boolean isRendering() { return rendering; } @Override protected void refreshAllPartRenderers(IRecordInternal[] records) { if (!isFormVisible || application.isShutDown() || rendering) return; // don't do anything yet when there are records but the selection is invalid if (formModel != null && (formModel.getSize() > 0 && (formModel.getSelectedIndex() < 0 || formModel.getSelectedIndex() >= formModel.getSize()))) return; // let the ui know that it will be touched, so that locks can be taken if needed. boolean executeOnRecordSelect = false; IRecordInternal[] state = records; if (state == null) { if (formModel != null) { state = new IRecordInternal[] { formModel.getPrototypeState() }; } else { state = new IRecordInternal[] { new PrototypeState(null) }; } } if (!(records == null && formModel != null && formModel.getRawSize() > 0) && isStateChanged(state)) { lastState = state; executeOnRecordSelect = true; } IDataAdapterList dataAdapterList = getFormUI().getDataAdapterList(); for (IRecordInternal r : state) dataAdapterList.setRecord(r, true); if (executeOnRecordSelect) { // do this at the end because dataRenderer.refreshRecord(state) will update selection // for related tabs - and we should execute js code after they have been updated executeOnRecordSelect(); } } @Override public void touch() { } @Override public void destroy() { if (getBasicFormManager() != null) getBasicFormManager().removeFormController(this); unload(); if (formUI != null) { formUI.destroy(); formUI = null; } super.destroy(); IWindow window = CurrentWindow.safeGet(); if (window instanceof NGClientWindow) { ((NGClientWindow)window).destroyForm(getName()); } } @Override protected void focusFirstField() { focusField(null, false); } @SuppressWarnings("nls") @Override protected void focusField(String fieldName, boolean skipReadonly) { WebComponent component = null; WebObjectApiDefinition apiFunction = null; if (fieldName != null) { component = formUI.getComponent(fieldName); if (component == null) { RuntimeWebComponent[] runtimeComponents = getWebComponentElements(); if (runtimeComponents != null) { for (RuntimeWebComponent runtimeComponent : runtimeComponents) { if (Utils.equalObjects(fieldName, runtimeComponent.getComponent().getName())) { component = runtimeComponent.getComponent(); break; } } } } if (component != null) { apiFunction = component.getSpecification().getApiFunction("requestFocus"); } } else { Collection<WebComponent> tabSequenceComponents = getTabSequenceComponents(); if (tabSequenceComponents != null) { for (WebComponent seqComponent : tabSequenceComponents) { apiFunction = seqComponent.getSpecification().getApiFunction("requestFocus"); if (apiFunction != null) { if (skipReadonly) { // TODO first https://support.servoy.com/browse/SVY-8024 should be fixed then this check should be on the property type. if (Boolean.TRUE.equals(component.getProperty("readOnly"))) { continue; } } component = seqComponent; break; } } } } if (apiFunction != null && component != null) component.invokeApi(apiFunction, null); } @Override public void propagateFindMode(boolean findMode) { if (!findMode) { application.getFoundSetManager().getEditRecordList().prepareForSave(true); } if (isReadOnly()) { // TODO should something happen here, should edit state be pushed or is that just handled in the find mode call? // if (view != null) // { // view.setEditable(findMode); // } } IDataAdapterList dal = getFormUI().getDataAdapterList(); dal.setFindMode(findMode); // disables related data en does getText instead if getValue on fields } @Override public void setReadOnly(boolean b) { if (b) stopUIEditing(true); formUI.setReadOnly(b); application.getFormManager().setFormReadOnly(getName(), b); } @Override public void setComponentEnabled(boolean b) { formUI.setComponentEnabled(b); application.getFormManager().setFormEnabled(getName(), b); } @Override public boolean recreateUI() { Form oldForm = form; // update flattened form reference cause now we probably need to use a SM modified version Form f = application.getFlattenedSolution().getForm(form.getName()); form = application.getFlattenedSolution().getFlattenedForm(f); INGClientWindow allWindowsProxy = new NGClientWebsocketSessionWindows(getApplication().getWebsocketSession()); if (allWindowsProxy.hasFormChangedSinceLastSendToClient(form, getName())) { // hide all visible children; here is an example that explains why it's needed: // parent form has tabpanel with child1 and child2; child2 is visible (second tab) // if you recreateUI on parent, child1 would turn out visible after recreateUI without a hide event on child2 if we wouldn't do the notifyVisible below; // but also when you would afterwards change tab to child2 it's onShow won't be called because it thinks it's still visible which is strange; List<Runnable> invokeLaterRunnables = new ArrayList<Runnable>(); notifyVisibleOnChildren(false, invokeLaterRunnables); Utils.invokeLater(application, invokeLaterRunnables); tabSequence = null; f = application.getFlattenedSolution().getForm(form.getName()); form = application.getFlattenedSolution().getFlattenedForm(f); getFormUI().init(); allWindowsProxy.updateForm(form, getName(), new FormHTMLAndJSGenerator(getApplication(), form, getName())); if (isFormVisible) { invokeLaterRunnables = new ArrayList<Runnable>(); notifyVisibleOnChildren(true, invokeLaterRunnables); Utils.invokeLater(application, invokeLaterRunnables); } application.getFlattenedSolution().deregisterLiveForm(form, namedInstance); application.getFlattenedSolution().registerLiveForm(form, namedInstance); } else { // in case it's not already loaded on client side - so we can't rely on endpoint URL differeces - but it is modified by Solution Model and recreateUI is called, it's formUI needs to reinitialize as well if ((oldForm != form || application.isInDeveloper()) && !allWindowsProxy.hasForm(getName())) { tabSequence = null; getFormUI().init(); } Debug.trace("RecreateUI on form " + getName() + " was ignored because that form was not changed since last being sent to client..."); } return true; } @Override public void refreshView() { } @Override public boolean getDesignMode() { // TODO Auto-generated method stub return false; } @Override public void setDesignMode(DesignModeCallbacks callback) { // TODO Auto-generated method stub } @Override public void setTabSequence(Object[] arrayOfElements) { if (arrayOfElements == null) { return; } Object[] elements = arrayOfElements; if (elements.length == 1) { if (elements[0] instanceof Object[]) { elements = (Object[])elements[0]; } else if (elements[0] == null) { elements = null; return; } } tabSequence = new String[elements.length]; for (int i = 0; i < elements.length; i++) { if (elements[i] instanceof RuntimeWebComponent) { WebFormComponent component = ((RuntimeWebComponent)elements[i]).getComponent(); WebObjectSpecification spec = component.getSpecification(); Collection<PropertyDescription> properties = spec.getProperties(NGTabSeqPropertyType.NG_INSTANCE); if (properties.size() == 1) { PropertyDescription pd = properties.iterator().next(); Integer val = Integer.valueOf(i + 1); if (!val.equals(component.getProperty(pd.getName()))) component.setProperty(pd.getName(), val); } tabSequence[i] = component.getName(); } else { Debug.error("Could not set the tab sequence property for element " + elements[i]); } } } private Collection<WebComponent> getTabSequenceComponents() { SortedList<WebComponent> orderedComponents = new SortedList<WebComponent>(new Comparator<WebComponent>() { @Override public int compare(WebComponent o1, WebComponent o2) { PropertyDescription pd1 = o1.getSpecification().getProperties(NGTabSeqPropertyType.NG_INSTANCE).iterator().next(); Integer val1 = (Integer)o1.getProperty(pd1.getName()); PropertyDescription pd2 = o2.getSpecification().getProperties(NGTabSeqPropertyType.NG_INSTANCE).iterator().next(); Integer val2 = (Integer)o2.getProperty(pd2.getName()); if (val1 == 0 && val2 == 0) { Point location1 = (Point)o1.getProperty(StaticContentSpecLoader.PROPERTY_LOCATION.getPropertyName()); Point location2 = (Point)o2.getProperty(StaticContentSpecLoader.PROPERTY_LOCATION.getPropertyName()); return PositionComparator.comparePoint(true, location1, location2); } return val1 - val2; } }); for (WebComponent component : formUI.getAllComponents()) { Collection<PropertyDescription> tabSeqProperties = component.getSpecification().getProperties(NGTabSeqPropertyType.NG_INSTANCE); if (tabSeqProperties.size() == 1) { Integer val1 = (Integer)component.getProperty(tabSeqProperties.iterator().next().getName()); if (val1 >= 0) { orderedComponents.add(component); } } } return orderedComponents; } @Override public String[] getTabSequence() { if (tabSequence == null) { Map<Integer, String> map = new TreeMap<Integer, String>(); boolean defaultTabSequence = true; for (WebComponent component : formUI.getScriptableComponents()) { WebObjectSpecification spec = component.getSpecification(); Collection<PropertyDescription> properties = spec.getProperties(NGTabSeqPropertyType.NG_INSTANCE); if (properties.size() == 1) { PropertyDescription pd = properties.iterator().next(); Integer value = (Integer)component.getProperty(pd.getName()); defaultTabSequence = defaultTabSequence && value.intValue() == 0; if (!component.getName().startsWith(FormElement.SVY_NAME_PREFIX) && value.intValue() > 0) { map.put(value, component.getName()); } } } if (defaultTabSequence) { ArrayList<String> sequence = new ArrayList<String>(); Iterator<IFormElement> it = form.getFormElementsSortedByFormIndex(); while (it.hasNext()) { IFormElement element = it.next(); if (element.getName() != null) sequence.add(element.getName()); } tabSequence = sequence.toArray(new String[sequence.size()]); } else { tabSequence = map.values().toArray(new String[map.size()]); } } return tabSequence; } @Override public int getPartYOffset(int partType) { int totalHeight = 0; for (Part part : Utils.iterate(getForm().getParts())) { if (part.getPartType() == partType) { break; } totalHeight = part.getHeight(); } return totalHeight; } @Override protected FormAndComponent getJSApplicationNames(Object source, Function function, boolean useFormAsEventSourceEventually) { Object src = source; if (src == null && useFormAsEventSourceEventually) src = formScope; return new FormAndComponent(src, getName()); } @Override protected JSEvent getJSEvent(Object src) { JSEvent event = new JSEvent(); event.setType(JSEvent.EventType.form); event.setFormName(getName()); event.setSource(src); if (src instanceof WebFormComponent) event.setElementName(((WebFormComponent)src).getFormElement().getRawName()); else event.setElementName(src instanceof WebComponent ? ((WebComponent)src).getName() : null); return event; } @Override public String toString() { if (formModel != null) { return "FormController[form: " + getName() + ", fs size:" + Integer.toString(formModel.getSize()) + ", selected record: " + //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ formModel.getRecord(formModel.getSelectedIndex()) + ",destroyed:" + isDestroyed() + "]"; //$NON-NLS-1$ } else { return "FormController[form: " + getName() + ",destroyed:" + isDestroyed() + "]"; //$NON-NLS-1$//$NON-NLS-2$ } } private WeakReference<IWebFormController> parentFormController; public void setParentFormController(IWebFormController parentFormController) { this.parentFormController = new WeakReference<IWebFormController>(parentFormController); } public IWebFormController getParentFormController() { if (parentFormController != null) { return parentFormController.get(); } return null; } @Override public boolean notifyVisible(boolean visible, List<Runnable> invokeLaterRunnables) { boolean notifyVisibleSuccess = super.notifyVisible(visible, invokeLaterRunnables); if (notifyVisibleSuccess) notifyVisibleOnChildren(visible, invokeLaterRunnables); // TODO should notifyVisibleSuccess be altered here? See WebFormUI/WebFormComponent notifyVisible calls. return notifyVisibleSuccess; } private boolean notifyVisibleOnChildren(boolean visible, List<Runnable> invokeLaterRunnables) { if (getFormUI() != null) { return getFormUI().notifyVisible(visible, invokeLaterRunnables); } return true; } private Map<String, Object> navigatorProperties; @Override public void setNavigatorProperties(Map<String, Object> navigatorDescription) { this.navigatorProperties = navigatorDescription; } @Override public Map<String, Object> getNavigatorProperties() { return navigatorProperties; } public RuntimeWebComponent[] getWebComponentElements() { Object elementScope = formScope == null ? null : formScope.get("elements"); if (elementScope instanceof DefaultScope) { Object[] values = ((DefaultScope)elementScope).getValues(); List<RuntimeWebComponent> elements = new ArrayList<RuntimeWebComponent>(values.length); for (Object value : values) { if (value instanceof Wrapper) { value = ((Wrapper)value).unwrap(); } if (value instanceof RuntimeWebComponent) { elements.add((RuntimeWebComponent)value); } } return elements.toArray(new RuntimeWebComponent[elements.size()]); } return new RuntimeWebComponent[0]; } }