/* * Copyright 2011 cruxframework.org. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.cruxframework.crux.core.client.screen.views; import java.util.logging.Level; import java.util.logging.Logger; import org.cruxframework.crux.core.client.Crux; import org.cruxframework.crux.core.client.Legacy; import org.cruxframework.crux.core.client.collection.Array; import org.cruxframework.crux.core.client.collection.CollectionFactory; import org.cruxframework.crux.core.client.collection.FastList; import org.cruxframework.crux.core.client.collection.FastMap; import org.cruxframework.crux.core.client.collection.Map; import org.cruxframework.crux.core.client.controller.RegisteredControllers; import org.cruxframework.crux.core.client.dataprovider.DataProvider; import org.cruxframework.crux.core.client.datasource.DataSource; import org.cruxframework.crux.core.client.formatter.RegisteredClientFormatters; import org.cruxframework.crux.core.client.ioc.IocContainer; import org.cruxframework.crux.core.client.resources.Resource; import org.cruxframework.crux.core.client.screen.InterfaceConfigException; import org.cruxframework.crux.core.client.screen.LazyPanelWrappingType; import org.cruxframework.crux.core.client.screen.binding.BindableContainer; import org.cruxframework.crux.core.client.screen.binding.DataBindingHandler; import org.cruxframework.crux.core.client.screen.binding.DataObjectBinder; import org.cruxframework.crux.core.client.utils.StringUtils; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.PartialSupport; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.event.shared.ResettableEventBus; import com.google.gwt.event.shared.SimpleEventBus; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.user.client.History; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.Window.ClosingEvent; import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.Widget; /** * @author Thiago da Rosa de Bustamante * */ public abstract class View implements HasViewResizeHandlers, HasWindowCloseHandlers, HasViewActivateHandlers, HasOrientationChangeHandler, HasViewLoadHandlers, BindableContainer { private static FastMap<View> loadedViews = new FastMap<View>(); private static Logger logger = Logger.getLogger(View.class.getName()); private static int prefixCounter = 0; private static FastMap<ClientBundle> resources = new FastMap<ClientBundle>(); protected boolean active = false; protected DataBindingHandler dataBindingHandler = null; protected Map<DataProvider<?>> dataProviders = null; protected ResettableEventBus eventBus = new ResettableEventBus(new SimpleEventBus()); protected String height; protected Map<String> lazyWidgets = null; protected boolean loaded = false; protected FastMap<Widget> widgets = null; @Deprecated protected FastList<ValueChangeHandler<String>> historyHandlers = new FastList<>(); protected String width; private String id; private String prefix; private String title; private ViewContainer viewContainer; /** * Constructor * @param id */ public View(String id) { this.id = id; this.prefix = Integer.toString(prefixCounter++); } @Override public void addDataObjectBinder(DataObjectBinder<?> dataObjectBinder, String dataObjectAlias) { dataBindingHandler.addDataObjectBinder(dataObjectBinder, dataObjectAlias); } /** * Add the given {@link DataProvider} to this View. * @param id an identifier for the {@link DataProvider}. * @param dataProvider the {@link DataProvider}. */ public void addDataProvider(String id, DataProvider<?> dataProvider) { if (dataProviders == null) { dataProviders = CollectionFactory.createMap(); } dataProviders.put(id, dataProvider); } @Override public HandlerRegistration addResizeHandler(final ResizeHandler handler) { return eventBus.addHandler(ResizeEvent.getType(), handler); } @Override public HandlerRegistration addViewActivateHandler(final ViewActivateHandler handler) { return eventBus.addHandler(ViewActivateEvent.getType(), handler); } @Override public HandlerRegistration addViewDeactivateHandler(final ViewDeactivateHandler handler) { return eventBus.addHandler(ViewDeactivateEvent.getType(), handler); } @Override public HandlerRegistration addViewLoadHandler(final ViewLoadHandler handler) { return eventBus.addHandler(ViewLoadEvent.getType(), handler); } /** * */ @Override public HandlerRegistration addViewUnloadHandler(final ViewUnloadHandler handler) { return eventBus.addHandler(ViewUnloadEvent.getType(), handler); } /** * Add a new widget into this view * @param id widget identifier * @param widget the widget */ public void addWidget(String id, IsWidget widget) { widgets.put(id, widget.asWidget()); } /** * Add a new widget into this view * @param id widget identifier * @param widget the widget */ public void addWidget(String id, Widget widget) { widgets.put(id, widget); } /** * * @param handler * @return */ @Override public HandlerRegistration addWindowCloseHandler(final CloseHandler<Window> handler) { return eventBus.addHandler(CloseEvent.getType(), handler); } /** * * @param handler * @return */ public HandlerRegistration addWindowClosingHandler(final ClosingHandler handler) { return eventBus.addHandler(new ClosingEvent().getAssociatedType(), handler); } /** * * @param handler * @return */ @Deprecated @Legacy public HandlerRegistration addWindowHistoryChangedHandler(final ValueChangeHandler<String> handler) { historyHandlers.add(handler); if (isActive()) { ViewHandlers.ensureViewContainerHistoryHandler(); } return new HandlerRegistration() { @Override public void removeHandler() { int index = historyHandlers.indexOf(handler); if (index >= 0) { historyHandlers.remove(index); } } }; } @Override @PartialSupport public HandlerRegistration addWindowOrientationChangeHandler(final OrientationChangeHandler handler) { if(!isOrientationChangeSupported()) { return null; } return eventBus.addHandler(OrientationChangeEvent.getType(), handler); } /** * Verify if the view contains an widget associated with the given identifier * @param id widget identifier * @return true if widget is present */ public boolean containsWidget(String id) { return widgets.containsKey(id); } @Override public void copyTo(Object dataObject) { dataBindingHandler.copyTo(dataObject); } /** * Create a new DataSource instance * @param dataSource dataSource name, declared with <code>@DataSource</code> annotation * @return new dataSource instance */ public abstract DataSource<?> createDataSource(String dataSource); /** * Retrieve the container that holds this view * @return Parent container or null if this view does not belong to any container. */ public ViewContainer getContainer() { return viewContainer; } /** * Retrieve the requested controller from this view * @param <T> Controller type * @param controller Controller name * @return */ public <T> T getController(String controller) { return getRegisteredControllers().getController(controller); } @Override public <T> DataObjectBinder<T> getDataObjectBinder(Class<T> dataObjectClass) { return dataBindingHandler.getDataObjectBinder(dataObjectClass); } @Override public <T> DataObjectBinder<T> getDataObjectBinder(String dataObjectAlias) { return dataBindingHandler.getDataObjectBinder(dataObjectAlias); } /** * Retrieve a {@link DataProvider} contained on this View * @param id the {@link DataProvider} identifier. * @return the {@link DataProvider} */ @SuppressWarnings("unchecked") public <T extends DataProvider<?>> T getDataProvider(String id) { if (dataProviders == null) { return null; } return (T) dataProviders.get(id); } /** * Retrieve the view height; * @return */ public String getHeight() { return height; } /** * Retrieve the view identifier * @return */ public String getId() { return id; } /** * Retrieve the IoCContainer instance associated with this view * @return */ public abstract IocContainer getIocContainer(); @Override public Widget getLoadedWidget(String id) { return getWidget(id, false); } /** * Retrieve the list of controllers registered into this view * @return */ public abstract RegisteredControllers getRegisteredControllers(); /** * Retrieve the list of formatters registered into this view * @return */ @Deprecated @Legacy public abstract RegisteredClientFormatters getRegisteredFormatters(); /** * Retrieve the view title * @return */ public String getTitle() { return title; } /** * Retrieve the main panel that contains all the components described into this view. * @return the viewPanel */ public abstract HTMLPanel getViewPanel(); /** * Retrieve a widget contained on this view. If the the requested widget does not exists, we check if * a request for a lazy creation of this widget was previously done. If so, we initialize the wrapper * required panel (according with {@code lazyWidgets} map) and try again. * * @param id widget identifier * @return the widget */ public Widget getWidget(String id) { assert(loaded):Crux.getMessages().viewNotInitialized(getId(), id); Widget widget = widgets.get(id); if (widget == null) { String lazyPanelId = lazyWidgets.get(id); if (lazyPanelId != null) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Found a lazy dependency. Widget["+id+"] depends on ["+lazyPanelId+"]."); } if (initializeLazyDependentWidget(lazyPanelId)) { widget = widgets.get(id); if (widget == null) { /* * If a lazyPanel contains as child a panel that is not visible, the enclosing * lazy panel of the child is only created when the external lazyPanel ensureWidget * method is called. It means that a new dependency was created during the initialization * of the first panel. We must check for this situation and add this new dependency here. */ widget = getRuntimeDependencyWidget(id, lazyPanelId); } } } } return widget; } /** * Retrieve a widget contained on this screen. * * @param id widget identifier * @param checkLazyDependencies if false, lazy dependencies will not be loaded * @return the widget */ public Widget getWidget(String id, boolean checkLazyDependencies) { if (checkLazyDependencies) { assert(loaded):Crux.getMessages().viewNotInitialized(getId(), id); return getWidget(id); } else { return widgets.get(id); } } /** * Retrieve a widget contained on this screen, casting it to the given class * * @param <T> * @param id widget identifier * @param clazz The class to be used to cast the widget * @return the widget */ @SuppressWarnings("unchecked") public <T extends IsWidget> T getWidget(String id, Class<T> clazz) { assert(loaded):Crux.getMessages().viewNotInitialized(getId(), id); Widget w = getWidget(id); return (T) w; } /** * Retrieve the view width; * @return */ public String getWidth() { return width; } /** * Reads the active property. * @return */ public boolean isActive() { return active; } /** * Return true if the view was loaded into a container. * @return */ public boolean isLoaded() { return loaded; } /** * @return */ public FastList<Widget> listWidgets() { FastList<String> keys = widgets.keys(); FastList<Widget> values = new FastList<Widget>(); for (int i=0; i<keys.size(); i++) { values.add(widgets.get(keys.get(i))); } return values; } /** * @return */ public FastList<String> listWidgetsId() { return widgets.keys(); } @Override public <T> T read(Class<T> dataObjectClass) { return dataBindingHandler.read(dataObjectClass); } @Override public <T> T read(String dataObjectAlias) { return dataBindingHandler.read(dataObjectAlias); } /** * Remove the given {@link DataProvider} from this View. * @param id the {@link DataProvider} identifier. */ public void removeDataProvider(String id) { if (dataProviders == null) { dataProviders.remove(id); } } /** * Remove the current view from its container, if the view is loaded into a container * @return true if the view is unloaded */ public boolean removeFromContainer() { if (viewContainer != null) { return viewContainer.remove(this); } return false; } /** * Removes the given widget from this view. * @param id widget identifier */ public void removeWidget(String id) { removeWidget(id, true); } /** * Removes the given widget from this view. * @param id widget identifier * @param removeFromDOM if true, also removes the widget from this parent widget */ public void removeWidget(String id, boolean removeFromDOM) { Widget widget = widgets.remove(id); if (widget != null && removeFromDOM) { widget.removeFromParent(); } dataBindingHandler.remove(id); } /** * Set the views height; * @return */ public void setHeight(String height) { this.height = height; if (isActive() && !StringUtils.isEmpty(height)) { updateViewHeight(height); } } /** * * @param title */ public void setTitle(String title) { this.title = title; } /** * Set the view width; * @return */ public void setWidth(String width) { this.width = width; if (isActive() && !StringUtils.isEmpty(width)) { updateViewWidth(width); } } /** * Retrieve a list with all widgets identifiers present into this view * @return */ public FastList<String> widgetsIdList() { return widgets.keys(); } /** * Retrieve a list with all widgets present into this view * @return */ public FastList<Widget> widgetsList() { FastList<String> keys = widgets.keys(); FastList<Widget> values = new FastList<Widget>(); for (int i=0; i<keys.size(); i++) { values.add(widgets.get(keys.get(i))); } return values; } @Override public void write(Object dataObject) { dataBindingHandler.write(dataObject); } @Override public void writeAll(Object... dataObjects) { dataBindingHandler.writeAll(dataObjects); } /** * When we have multi-level inner lazy panels, the most inside panel is dependent from the most outside one. * If the most outside is loaded, a new dependency must be created for the inner lazy panels not yet * loaded. * @param id * @param lazyPanelId */ protected void checkRuntimeLazyDependency(String id, String lazyPanelId) { if (!lazyWidgets.containsKey(id)) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "New runtime lazy dependency found. Widget["+id+"] depends on LazyPanel["+lazyPanelId+"]..."); } lazyWidgets.put(id, lazyPanelId); } } /** * @param widgetId */ protected void cleanLazyDependentWidgets(String widgetId) { if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Cleaning lazy dependencies of lazyPanel ["+widgetId+"]..."); } FastList<String> dependentWidgets = getDependentWidgets(widgetId); for (int i=0; i<dependentWidgets.size(); i++) { lazyWidgets.remove(dependentWidgets.get(i)); } if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Lazy dependencies of lazyPanel ["+widgetId+"] removed."); } } /** * When view is unloaded, we must free its allocated memory. */ protected void clearViewObjects() { lazyWidgets = null; widgets = null; dataBindingHandler = null; eventBus.removeHandlers(); } /** * Called by View container to create the view widgets * */ protected abstract void createWidgets() throws InterfaceConfigException; protected void fireActivateEvent(final ViewActivateEvent event) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { eventBus.fireEvent(event); viewContainer.fireActivateEvent(event); } }); } protected void fireDeactivateEvent(ViewDeactivateEvent event) { eventBus.fireEvent(event); viewContainer.fireDeactivateEvent(event); } /** * * @param event */ protected void fireHistoryChangeEvent(ValueChangeEvent<String> event) { for (int i = 0; i < historyHandlers.size(); i++) { ValueChangeHandler<String> handler = historyHandlers.get(i); handler.onValueChange(event); } } /** * Fires the load event */ protected void fireLoadEvent(ViewLoadEvent event) { eventBus.fireEvent(event); } protected void fireOrientationEvent(String orientation) { OrientationChangeEvent.fire(this, orientation); } protected void fireOrientationChangeEvent(OrientationChangeEvent event) { eventBus.fireEvent(event); } /** * * @param event */ protected void fireResizeEvent(ResizeEvent event) { eventBus.fireEvent(event); } /** * Fires the unload event. * @param event */ protected void fireUnloadEvent(ViewUnloadEvent event) { eventBus.fireEvent(event); viewContainer.fireUnloadEvent(event); } /** * * @param event */ protected void fireWindowCloseEvent(CloseEvent<Window> event) { eventBus.fireEvent(event); } /** * * @param event */ protected void fireWindowClosingEvent(ClosingEvent event) { ViewUnloadEvent.fire(this); eventBus.fireEvent(event); } /** * Retrieve the view prefix. It is used to isolate all elements from this view on DOM * @return */ protected String getPrefix() { return prefix; } /** * Called when the view are loaded to initialize the lazy dependencies map */ protected abstract Map<String> initializeLazyDependencies(); /** * Call the {@code LazyPanel.ensureWidget()} method of the given lazyPanel. * This method can trigger other dependent lazyPanel initialization, through * a recursive call to {@code View.getWidget(String)}. * * @param widgetId lazyPanel to be loaded * @return true if some lazyPanel was really loaded for this request */ protected boolean initializeLazyDependentWidget(String widgetId) { boolean ret = false; if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, "Initializing lazy dependents widgets of lazyPanel ["+widgetId+"]..."); } LazyPanel lazyPanel = (LazyPanel) widgets.get(widgetId); if (lazyPanel == null) { //TODO: stackoverflow error when trying to access widgets on onLoad from a view (inside an HTMLPanel) if (getWidget(ViewFactoryUtils.getWrappedWidgetIdFromLazyPanel(widgetId)) != null) { lazyPanel = (LazyPanel) widgets.get(widgetId); } } if (lazyPanel != null) { lazyPanel.ensureWidget(); ret = true; } else { cleanLazyDependentWidgets(widgetId); } if (LogConfiguration.loggingIsEnabled()) { logger.log(Level.FINE, " Lazy dependents widgets of lazyPanel ["+widgetId+"] are now loaded."); } return ret; } /** * Called by the {@link ViewContainer} when the view is added to the container. * This method creates the view widgets * @param parameter parameter sent to view and accessible through ViewLoadEvent event * @return true if loaded */ protected boolean load(Object parameter) { if (!loaded) { prepareViewObjects(); registerDataObjectBinders(); registerLoadedView(); createWidgets(); loaded = true; ViewLoadEvent.fire(this, parameter); } return loaded; } protected void prepareViewObjects() { lazyWidgets = initializeLazyDependencies(); widgets = new FastMap<Widget>(); dataBindingHandler = new DataBindingHandler(this); } /** * Called when the view are loaded to initialize the dataObjectBinders */ protected abstract void registerDataObjectBinders(); /** * Register current view into the loaded views list */ protected void registerLoadedView() { loadedViews.put(getId(), this); } /** * Called by View container to render the view into the screen * @param rootPanel The root element where the view elements will be rendered. */ protected abstract void render(Panel rootPanel, RenderCallback renderCallback); /** * Mark this view as active * @param parameter to be passed to activate event */ protected void setActive(Object parameter) { if (!active) { active = true; ViewActivateEvent.fire(this, parameter);; } } /** * Bind the view to a container. Called by the {@link ViewContainer} add method. * @param viewContainer */ protected void setContainer(ViewContainer viewContainer) { this.viewContainer = viewContainer; } /** * Mark this view as active * @param skipEvent */ protected boolean setDeactivated(boolean skipEvent) { if (active) { if (!skipEvent) { ViewDeactivateEvent event = ViewDeactivateEvent.fire(this); active = event.isCanceled(); return !active; } else { active = false; } } return true; } /** * Called by the {@link ViewContainer} when the view is removed from the container. * @return true if the view is not loaded */ protected boolean unload() { boolean unloaded = true; if (this.loaded) { ViewUnloadEvent event = ViewUnloadEvent.fire(this); unloaded = !event.isCanceled(); if (unloaded) { unregisterLoadedView(); clearViewObjects(); loaded = false; } } return unloaded; } /** * Remove current view from the loaded views list */ protected void unregisterLoadedView() { loadedViews.remove(getId()); } protected abstract void updateViewHeight(String height); protected abstract void updateViewWidth(String width); /** * @param widgetId * @return */ private FastList<String> getDependentWidgets(String widgetId) { FastList<String> dependentWidgets = new FastList<String>(); Array<String> keys = lazyWidgets.keys(); int size = keys.size(); for (int i=0; i<size; i++) { String key = keys.get(i); if (lazyWidgets.get(key).equals(widgetId)) { dependentWidgets.add(key); } } return dependentWidgets; } /** * If a lazyPanel contains as child a panel that is not visible, the enclosing * lazy panel of the child is only created when the external lazyPanel ensureWidget * method is called. It means that a new dependency was created during the initialization * of the first panel. We must check for this situation and add this new dependency here. * * @param id * @param lazyPanelId * @return */ private Widget getRuntimeDependencyWidget(String id, String lazyPanelId) { Widget widget = null; if (ViewFactoryUtils.isWholeWidgetLazyWrapper(lazyPanelId)) { lazyPanelId = ViewFactoryUtils.getWrappedWidgetIdFromLazyPanel(lazyPanelId); lazyPanelId = ViewFactoryUtils.getLazyPanelId(lazyPanelId, LazyPanelWrappingType.wrapChildren); if (widgets.containsKey(lazyPanelId)) { /* When the internal lazy dependency created is derived from a LazyPanelWrappingType.wrapChildren lazy instantiation.*/ lazyWidgets.put(id, lazyPanelId); widget = getWidget(id); } else { /* Check if the internal lazy dependency created is derived from a LazyPanelWrappingType.wrapWholeWidget lazy instantiation.*/ widget = getRuntimeDependencyWidgetFromWholeWidget(id); } } else { /* Check if the internal lazy dependency created is derived from a LazyPanelWrappingType.wrapWholeWidget lazy instantiation.*/ widget = getRuntimeDependencyWidgetFromWholeWidget(id); } return widget; } /** * When the internal lazy dependency created is derived from a LazyPanelWrappingType.wrapWholeWidget * lazy instantiation. * * @param id * @return */ private Widget getRuntimeDependencyWidgetFromWholeWidget(String id) { Widget widget = null; String lazyPanelId; lazyPanelId = ViewFactoryUtils.getLazyPanelId(id, LazyPanelWrappingType.wrapWholeWidget); if (widgets.containsKey(lazyPanelId)) { lazyWidgets.put(id, lazyPanelId); widget = getWidget(id); } return widget; } /** * Adds a new token for history control. * @param token */ public static void addToHistory(String token) { History.newItem(token, false); } /** * Adds a new token for history control. * @param token * @param issueEvent */ public static void addToHistory(String token, boolean issueEvent) { History.newItem(token, issueEvent); } /** * Returns true if a resource associated with the given identifiers was loaded by application * @param id * @return */ public static boolean containsResource(String id) { return resources.containsKey(id); } /** * Retrieve the client bundle associated with the given id. To map a client bundle interface to an identifier, use * the {@link Resource} annotation * @param id * @return */ public static ClientBundle getResource(String id) { return resources.get(id); } /** * Retrieve the view by its identifier * @param id view identifier * @return the view or null if there is no view loaded into the screen with this identifier */ @SuppressWarnings("unchecked") public static <T extends View> T getView(String id) { return (T) loadedViews.get(id); } /** * @return true is this page supports orientationChange and false otherwise. */ public static native boolean isOrientationChangeSupported()/*-{ return "onorientationchange" in $wnd || "orientationchange" in $wnd || "ondeviceorientation" in $wnd || "deviceorientation" in $wnd; }-*/; /** * Retrieve the current view associated with a controller, datasource, or other ViewAware object * @param viewAware * @return */ @SuppressWarnings("unchecked") public static <T extends View> T of(Object viewAware) { assert (viewAware instanceof ViewAware): Crux.getMessages().viewOjectIsNotAwareOfView(); return (T) ((ViewAware)viewAware).getBoundCruxView(); } /** * * @param id * @param resource */ protected static void addResource(String id, ClientBundle resource) { resources.put(id, resource); } public static interface RenderCallback { void onRendered(); } }