diff --git a/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/NavigationRequestBuilder.java b/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/NavigationRequestBuilder.java index 0c437a5..b3fb591 100644 --- a/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/NavigationRequestBuilder.java +++ b/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/NavigationRequestBuilder.java @@ -1,334 +1,334 @@ -/* - * Copyright (c) 2011 Petter Holmström - * - * 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 com.github.peholmst.mvp4vaadin.navigation; - -import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import com.github.peholmst.mvp4vaadin.View; - -/** - * This class implements a builder for creating new {@link NavigationRequest} - * instances. This is how it is to be used, using the default path builder: - * - *

- * Other path builders can be plugged in by using the - * {@link #newInstance(Class)} factory method. - * - * @author Petter Holmström - * @since 1.0 - */ -public final class NavigationRequestBuilder

{ - - private final Class

pathBuilderClass; - - private P pathBuilder; - - private final HashMap params = new HashMap(); - - private NavigationRequestBuilder(Class

pathBuilderClass) { - this.pathBuilderClass = pathBuilderClass; - } - - private P createPathBuilder() { - try { - final Constructor

constructor = pathBuilderClass - .getConstructor(NavigationRequestBuilder.class); - return constructor.newInstance(this); - } catch (Exception e) { - throw new RuntimeException("Could not create path builder", e); - } - } - - private P createPathBuilder(List initialPath) { - try { - final Constructor

constructor = pathBuilderClass.getConstructor( - NavigationRequestBuilder.class, List.class); - return constructor.newInstance(this, initialPath); - } catch (Exception e) { - throw new RuntimeException("Could not create path builder", e); - } - } - - /** - * Base class for a path builder that builds the - * {@link NavigationRequest#getPath() path} of a {@link NavigationRequest}. - * - * @author Petter Holmström - * @since 1.0 - */ - public static abstract class PathBuilder { - - private final NavigationRequestBuilder requestBuilder; - private final LinkedList path = new LinkedList(); - - /** - * Creates a new PathBuilder. - * - * @param requestBuilder - * the owning request builder. - */ - public PathBuilder(NavigationRequestBuilder requestBuilder) { - this.requestBuilder = requestBuilder; - } - - /** - * Creates a new PathBuilder. - * - * @param requestBuilder - * the owning request builder. - * @param initialPath - * the initial path. - */ - public PathBuilder(NavigationRequestBuilder requestBuilder, - List initialPath) { - this(requestBuilder); - path.addAll(initialPath); - } - - /** - * Returns the path to which views can be added. - */ - protected final LinkedList getPath() { - return path; - } - - /** - * Builds a {@link NavigationRequest} instance for the current path and - * returns it. - * - * @throws IllegalStateException - * if the path is empty. - */ - @SuppressWarnings("unchecked") - public NavigationRequest buildRequest() throws IllegalStateException { - if (getPath().isEmpty()) { - throw new IllegalStateException( - "The path must contain at least one view"); - } - final List copyOfPath = Collections - .unmodifiableList((List) getPath().clone()); - final Map copyOfParams = Collections - .unmodifiableMap((Map) requestBuilder.params - .clone()); - return new NavigationRequest() { - - private static final long serialVersionUID = -6273102646598049858L; - - @Override - public List getPath() { - return copyOfPath; - } - - @Override - public Map getParams() { - return copyOfParams; - } - }; - } - - } - - /** - * A path builder is used to construct the path of the - * {@link NavigationRequest}. The request itself is built by calling the - * {@link #buildRequest()} method. - * - * @author Petter Holmström - * @since 1.0 - */ - public static final class DefaultPathBuilder extends PathBuilder { - - public DefaultPathBuilder(NavigationRequestBuilder requestBuilder) { - super(requestBuilder); - } - - public DefaultPathBuilder(NavigationRequestBuilder requestBuilder, - List initialPath) { - super(requestBuilder, initialPath); - } - - /** - * Adds the specified view to the path. - */ - public DefaultPathBuilder addViewToPath(View view) { - getPath().add(view); - return this; - } - - /** - * Adds the specified views to the path. - */ - public DefaultPathBuilder addViewsToPath(View... views) { - getPath().addAll(Arrays.asList(views)); - return this; - } - } - - /** - * Sets the value of a single parameter to be passed to the view. - */ - public NavigationRequestBuilder

setParam(String paramName, - Object paramValue) { - params.put(paramName, paramValue); - return this; - } - - /** - * Sets the values of multiple parameters to be passed to the view. - */ - public NavigationRequestBuilder

setParams(Map params) { - params.putAll(params); - return this; - } - - /** - * Returns a {@link PathBuilder} that starts from the previous view (i.e. - * the view behind the current view) of the specified view controller. This - * path can be used to perform a "go back" navigation. - * - * @throws IllegalStateException - * if there are less than two views in the controller's stack, - * or if another path builder has already been created. - */ - public P startWithPathToPreviousView(NavigationController controller) - throws IllegalStateException { - if (controller.getViewStack().size() < 2) { - throw new IllegalStateException( - "Not enough views in controller to start from the previous view"); - } - verifyPathBuilderNotSet(); - pathBuilder = createPathBuilder(controller.getViewStack().subList(0, - controller.getViewStack().size() - 1)); - return pathBuilder; - } - - /** - * Returns a {@link PathBuilder} that starts from the first view of the - * specified view controller. This path can be used to perform a "go home" - * navigation. - * - * @throws IllegalStateException - * if the controller's stack is empty, or if another path - * builder has already been created. - */ - public P startWithPathToFirstView(NavigationController controller) - throws IllegalStateException { - if (controller.isEmpty()) { - throw new IllegalStateException( - "Controller is empty, cannot start from the first view"); - } - verifyPathBuilderNotSet(); - pathBuilder = createPathBuilder(controller.getViewStack().subList(0, 1)); - return pathBuilder; - } - - /** - * Returns a {@link PathBuilder} that starts from the current view of the - * specified view controller. This path can be used when adding a new view - * to the stack. If the controller is empty, this call has the same effect - * as using {@link #startWithEmptyPath()}. - * - * @throws IllegalStateException - * if another path builder has already been created. - */ - public P startWithPathToCurrentView(NavigationController controller) - throws IllegalStateException { - verifyPathBuilderNotSet(); - pathBuilder = createPathBuilder(controller.getViewStack()); - return pathBuilder; - } - - /** - * Returns a {@link PathBuilder} that starts from the path to the specified - * view. - * - * @throws IllegalStateException - * if another path builder has already been created or the - * specified view cannot be found in the controller. - */ - public P startWithPathToView(NavigationController controller, View view) - throws IllegalStateException { - verifyPathBuilderNotSet(); - LinkedList path = new LinkedList(); - boolean found = false; - for (View viewInPath : controller.getViewStack()) { - path.add(viewInPath); - if (viewInPath.equals(view)) { - found = true; - break; - } - } - if (!found) { - throw new IllegalStateException("View not found in controller"); - } - pathBuilder = createPathBuilder(path); - return pathBuilder; - } - - /** - * Returns a {@link PathBuilder} that starts with an empty path. At least - * one view has to be added before the navigation request can be built. - * - * @throws IllegalStateException - * if another path builder has already been created. - */ - public P startWithEmptyPath() throws IllegalStateException { - verifyPathBuilderNotSet(); - pathBuilder = createPathBuilder(); - return pathBuilder; - } - - private void verifyPathBuilderNotSet() throws IllegalStateException { - if (pathBuilder != null) { - throw new IllegalStateException( - "A pathBuilder has already been created"); - } - } - - /** - * Returns a new navigation request builder instance that uses the default - * path builder. - */ - public static NavigationRequestBuilder newInstance() { - return new NavigationRequestBuilder( - DefaultPathBuilder.class); - } - - /** - * Returns a new navigation request builder instance that uses a path - * builder of the specified class. - */ - public static

NavigationRequestBuilder

newInstance( - Class

pathBuilderClass) { - return new NavigationRequestBuilder

(pathBuilderClass); - } -} +/* + * Copyright (c) 2011 Petter Holmström + * + * 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 com.github.peholmst.mvp4vaadin.navigation; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.github.peholmst.mvp4vaadin.View; + +/** + * This class implements a builder for creating new {@link NavigationRequest} + * instances. This is how it is to be used, using the default path builder: + *

+ *

+ * Other path builders can be plugged in by using the + * {@link #newInstance(Class)} factory method. + * + * @author Petter Holmström + * @since 1.0 + */ +public final class NavigationRequestBuilder

{ + + private final Class

pathBuilderClass; + + private P pathBuilder; + + private final HashMap params = new HashMap(); + + private NavigationRequestBuilder(Class

pathBuilderClass) { + this.pathBuilderClass = pathBuilderClass; + } + + private P createPathBuilder() { + try { + final Constructor

constructor = pathBuilderClass + .getConstructor(NavigationRequestBuilder.class); + return constructor.newInstance(this); + } catch (Exception e) { + throw new RuntimeException("Could not create path builder", e); + } + } + + private P createPathBuilder(List initialPath) { + try { + final Constructor

constructor = pathBuilderClass.getConstructor( + NavigationRequestBuilder.class, List.class); + return constructor.newInstance(this, initialPath); + } catch (Exception e) { + throw new RuntimeException("Could not create path builder", e); + } + } + + /** + * Base class for a path builder that builds the + * {@link NavigationRequest#getPath() path} of a {@link NavigationRequest}. + * + * @author Petter Holmström + * @since 1.0 + */ + public static abstract class PathBuilder { + + private final NavigationRequestBuilder requestBuilder; + private final LinkedList path = new LinkedList(); + + /** + * Creates a new PathBuilder. + * + * @param requestBuilder + * the owning request builder. + */ + public PathBuilder(NavigationRequestBuilder requestBuilder) { + this.requestBuilder = requestBuilder; + } + + /** + * Creates a new PathBuilder. + * + * @param requestBuilder + * the owning request builder. + * @param initialPath + * the initial path. + */ + public PathBuilder(NavigationRequestBuilder requestBuilder, + List initialPath) { + this(requestBuilder); + path.addAll(initialPath); + } + + /** + * Returns the path to which views can be added. + */ + protected final LinkedList getPath() { + return path; + } + + /** + * Builds a {@link NavigationRequest} instance for the current path and + * returns it. + * + * @throws IllegalStateException + * if the path is empty. + */ + @SuppressWarnings("unchecked") + public NavigationRequest buildRequest() throws IllegalStateException { + if (getPath().isEmpty()) { + throw new IllegalStateException( + "The path must contain at least one view"); + } + final List copyOfPath = Collections + .unmodifiableList((List) getPath().clone()); + final Map copyOfParams = Collections + .unmodifiableMap((Map) requestBuilder.params + .clone()); + return new NavigationRequest() { + + private static final long serialVersionUID = -6273102646598049858L; + + @Override + public List getPath() { + return copyOfPath; + } + + @Override + public Map getParams() { + return copyOfParams; + } + }; + } + + } + + /** + * A path builder is used to construct the path of the + * {@link NavigationRequest}. The request itself is built by calling the + * {@link #buildRequest()} method. + * + * @author Petter Holmström + * @since 1.0 + */ + public static final class DefaultPathBuilder extends PathBuilder { + + public DefaultPathBuilder(NavigationRequestBuilder requestBuilder) { + super(requestBuilder); + } + + public DefaultPathBuilder(NavigationRequestBuilder requestBuilder, + List initialPath) { + super(requestBuilder, initialPath); + } + + /** + * Adds the specified view to the path. + */ + public DefaultPathBuilder addViewToPath(View view) { + getPath().add(view); + return this; + } + + /** + * Adds the specified views to the path. + */ + public DefaultPathBuilder addViewsToPath(View... views) { + getPath().addAll(Arrays.asList(views)); + return this; + } + } + + /** + * Sets the value of a single parameter to be passed to the view. + */ + public NavigationRequestBuilder

setParam(String paramName, + Object paramValue) { + params.put(paramName, paramValue); + return this; + } + + /** + * Sets the values of multiple parameters to be passed to the view. + */ + public NavigationRequestBuilder

setParams(Map params) { + this.params.putAll(params); + return this; + } + + /** + * Returns a {@link PathBuilder} that starts from the previous view (i.e. + * the view behind the current view) of the specified view controller. This + * path can be used to perform a "go back" navigation. + * + * @throws IllegalStateException + * if there are less than two views in the controller's stack, + * or if another path builder has already been created. + */ + public P startWithPathToPreviousView(NavigationController controller) + throws IllegalStateException { + if (controller.getViewStack().size() < 2) { + throw new IllegalStateException( + "Not enough views in controller to start from the previous view"); + } + verifyPathBuilderNotSet(); + pathBuilder = createPathBuilder(controller.getViewStack().subList(0, + controller.getViewStack().size() - 1)); + return pathBuilder; + } + + /** + * Returns a {@link PathBuilder} that starts from the first view of the + * specified view controller. This path can be used to perform a "go home" + * navigation. + * + * @throws IllegalStateException + * if the controller's stack is empty, or if another path + * builder has already been created. + */ + public P startWithPathToFirstView(NavigationController controller) + throws IllegalStateException { + if (controller.isEmpty()) { + throw new IllegalStateException( + "Controller is empty, cannot start from the first view"); + } + verifyPathBuilderNotSet(); + pathBuilder = createPathBuilder(controller.getViewStack().subList(0, 1)); + return pathBuilder; + } + + /** + * Returns a {@link PathBuilder} that starts from the current view of the + * specified view controller. This path can be used when adding a new view + * to the stack. If the controller is empty, this call has the same effect + * as using {@link #startWithEmptyPath()}. + * + * @throws IllegalStateException + * if another path builder has already been created. + */ + public P startWithPathToCurrentView(NavigationController controller) + throws IllegalStateException { + verifyPathBuilderNotSet(); + pathBuilder = createPathBuilder(controller.getViewStack()); + return pathBuilder; + } + + /** + * Returns a {@link PathBuilder} that starts from the path to the specified + * view. + * + * @throws IllegalStateException + * if another path builder has already been created or the + * specified view cannot be found in the controller. + */ + public P startWithPathToView(NavigationController controller, View view) + throws IllegalStateException { + verifyPathBuilderNotSet(); + LinkedList path = new LinkedList(); + boolean found = false; + for (View viewInPath : controller.getViewStack()) { + path.add(viewInPath); + if (viewInPath.equals(view)) { + found = true; + break; + } + } + if (!found) { + throw new IllegalStateException("View not found in controller"); + } + pathBuilder = createPathBuilder(path); + return pathBuilder; + } + + /** + * Returns a {@link PathBuilder} that starts with an empty path. At least + * one view has to be added before the navigation request can be built. + * + * @throws IllegalStateException + * if another path builder has already been created. + */ + public P startWithEmptyPath() throws IllegalStateException { + verifyPathBuilderNotSet(); + pathBuilder = createPathBuilder(); + return pathBuilder; + } + + private void verifyPathBuilderNotSet() throws IllegalStateException { + if (pathBuilder != null) { + throw new IllegalStateException( + "A pathBuilder has already been created"); + } + } + + /** + * Returns a new navigation request builder instance that uses the default + * path builder. + */ + public static NavigationRequestBuilder newInstance() { + return new NavigationRequestBuilder( + DefaultPathBuilder.class); + } + + /** + * Returns a new navigation request builder instance that uses a path + * builder of the specified class. + */ + public static

NavigationRequestBuilder

newInstance( + Class

pathBuilderClass) { + return new NavigationRequestBuilder

(pathBuilderClass); + } +} diff --git a/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/ui/Breadcrumbs.java b/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/ui/Breadcrumbs.java index 36df074..56dfebb 100644 --- a/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/ui/Breadcrumbs.java +++ b/Sources/MVP4Vaadin/src/com/github/peholmst/mvp4vaadin/navigation/ui/Breadcrumbs.java @@ -1,293 +1,293 @@ -/* - * Copyright (c) 2011 Petter Holmström - * - * 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 com.github.peholmst.mvp4vaadin.navigation.ui; - -import java.util.HashMap; -import java.util.Map; - -import com.github.peholmst.mvp4vaadin.View; -import com.github.peholmst.mvp4vaadin.ViewEvent; -import com.github.peholmst.mvp4vaadin.ViewListener; -import com.github.peholmst.mvp4vaadin.events.DescriptionChangedViewEvent; -import com.github.peholmst.mvp4vaadin.events.DisplayNameChangedViewEvent; -import com.github.peholmst.mvp4vaadin.navigation.NavigationController; -import com.github.peholmst.mvp4vaadin.navigation.NavigationControllerEvent; -import com.github.peholmst.mvp4vaadin.navigation.NavigationControllerListener; -import com.github.peholmst.mvp4vaadin.navigation.NavigationRequest; -import com.github.peholmst.mvp4vaadin.navigation.NavigationRequestBuilder; -import com.github.peholmst.mvp4vaadin.navigation.events.CurrentNavigationControllerViewChangedEvent; -import com.vaadin.ui.Alignment; -import com.vaadin.ui.Button; -import com.vaadin.ui.Component; -import com.vaadin.ui.HorizontalLayout; -import com.vaadin.ui.Label; -import com.vaadin.ui.Button.ClickEvent; -import com.vaadin.ui.themes.BaseTheme; - -/** - * This class implements a breadcrumb navigation bar that shows all the views - * currently in a {@link NavigationController} as links in a row, where the - * first view corresponds to the first link, etc: - * - *

- * First view >> Second view >> Third view >> ...
- * 
- *

- * As the views change in the controller, the navigation bar will update itself. - * A click on any of the navigation links will request the view controller to - * navigate to that particular view. - *

- * Both the links and the separators can be customized by implementing the - * {@link ButtonFactory} and {@link SeparatorFactory} interfaces, respectively. - * - * @see #setController(NavigationController) - * - * @author Petter Holmström - * @since 1.0 - */ -public class Breadcrumbs extends HorizontalLayout implements - NavigationControllerListener, ViewListener { - - private static final long serialVersionUID = 4513495936876605206L; - - public static final String BREADCRUMB_ELEMENT = "breadcrumb-element"; - - /** - * Factory interface for creating breadcrumb separators. - * - * @see Breadcrumbs#setSeparatorFactory(SeparatorFactory) - * @author Petter Holmström - * @since 1.0 - */ - public static interface SeparatorFactory extends java.io.Serializable { - /** - * Creates and returns a component to be used as a separator between - * breadcrumbs. - */ - Component createSeparator(); - } - - /** - * Default implementation of {@link SeparatorFactory}. The separators are - * labels containing the "»" character and having the - * {@link Breadcrumbs#BREADCRUMB_ELEMENT} style. - * - * @author Petter Holmström - * @since 1.0 - */ - public static class DefaultSeparatorFactory implements SeparatorFactory { - - private static final long serialVersionUID = 7957216244739746986L; - - @Override - public Component createSeparator() { - final Label separator = new Label("»"); - separator.setSizeUndefined(); - separator.addStyleName(BREADCRUMB_ELEMENT); - return separator; - } - } - - /** - * Factory interface for creating breadcrumb buttons. - * - * @see Breadcrumbs#setButtonFactory(ButtonFactory) - * @author Petter Holmström - * @since 1.0 - */ - public static interface ButtonFactory extends java.io.Serializable { - - /** - * Creates and returns a button for the specified view. The click - * listener will be registered by the breadcrumbs component. - */ - Button createButton(View view); - - /** - * Updates the button texts. This method is called when the display name - * and/or the description of the specified view are changed. - */ - void updateButtonTexts(Button button, View view); - } - - /** - * Default implementation of {@link ButtonFactory}. The created buttons have - * the {@link BaseTheme#BUTTON_LINK} and - * {@link Breadcrumbs#BREADCRUMB_ELEMENT} styles. - * - * @author Petter Holmström - * @since 1.0 - */ - public static class DefaultButtonFactory implements ButtonFactory { - - private static final long serialVersionUID = 8031407455065485896L; - - @Override - public Button createButton(View view) { - final Button btn = new Button(); - btn.setStyleName(BaseTheme.BUTTON_LINK); - btn.setSizeUndefined(); - btn.addStyleName(BREADCRUMB_ELEMENT); - updateButtonTexts(btn, view); - return btn; - } - - @Override - public void updateButtonTexts(Button button, View view) { - button.setCaption(view.getDisplayName()); - button.setDescription(view.getViewDescription()); - } - } - - private SeparatorFactory separatorFactory = new DefaultSeparatorFactory(); - - private ButtonFactory buttonFactory = new DefaultButtonFactory(); - - private NavigationController controller; - - private Map viewButtonMap = new HashMap(); - - /** - * Returns the navigation controller whose view stack will be displayed as - * breadcrumbs. If no controller has been set, null is - * returned. - */ - public NavigationController getController() { - return controller; - } - - /** - * Sets the navigation controller to use. This component will register - * itself as a listener of the controller. Setting the controller to - * null will unregister the listener. - */ - public void setController(NavigationController controller) { - if (this.controller != null) { - this.controller.removeListener(this); - } - this.controller = controller; - addBreadcrumbsForControllerRemovingAnyExistingOnes(); - if (this.controller != null) { - this.controller.addListener(this); - } - } - - /** - * Returns the separator factory to use for creating separators between - * breadcrumb buttons. - */ - public SeparatorFactory getSeparatorFactory() { - return separatorFactory; - } - - /** - * Sets the separator factory to use for creating separators between - * breadcrumb buttons. Set this value to null to use the - * default separator factory. - */ - public void setSeparatorFactory(SeparatorFactory separatorFactory) { - if (separatorFactory == null) { - separatorFactory = new DefaultSeparatorFactory(); - } - this.separatorFactory = separatorFactory; - } - - /** - * Returns the button factory to use for creating breadcrumb buttons. - */ - public ButtonFactory getButtonFactory() { - return buttonFactory; - } - - /** - * Sets the button factory to use for creating breadcrumb buttons. Set this - * value to null to use the default button factory. - */ - public void setButtonFactory(ButtonFactory buttonFactory) { - if (buttonFactory == null) { - buttonFactory = new DefaultButtonFactory(); - } - this.buttonFactory = buttonFactory; - } - - private void addBreadcrumbsForControllerRemovingAnyExistingOnes() { - removeBreadcrumbs(); - if (getController() != null) { - for (View view : getController().getViewStack()) { - addBreadcrumbForView(view); - } - } - } - - @Override - public void handleNavigationControllerEvent(NavigationControllerEvent event) { - if (event.getSource() != getController() - || !(event instanceof CurrentNavigationControllerViewChangedEvent)) { - return; - } - // TODO Maybe this method should be optimized so that not all buttons - // need to be removed unless absolutely necessary. - addBreadcrumbsForControllerRemovingAnyExistingOnes(); - } - - protected void addBreadcrumbForView(final View view) { - addSeparatorForView(view); - final Button btn = getButtonFactory().createButton(view); - final NavigationRequest navigationRequest = NavigationRequestBuilder - .newInstance().startWithPathToView(getController(), view) - .buildRequest(); - btn.addListener(new Button.ClickListener() { - - private static final long serialVersionUID = 5199653237630939848L; - - @Override - public void buttonClick(ClickEvent event) { - getController().navigate(navigationRequest); - } - }); - viewButtonMap.put(view, btn); - view.addListener(this); - addComponent(btn); - setComponentAlignment(btn, Alignment.MIDDLE_LEFT); - } - - protected void addSeparatorForView(final View view) { - if (getController().containsMoreThanOneElement()) { - Component separator = getSeparatorFactory().createSeparator(); - addComponent(separator); - setComponentAlignment(separator, Alignment.MIDDLE_LEFT); - } - } - - protected void removeBreadcrumbs() { - removeAllComponents(); - for (View view : viewButtonMap.keySet()) { - view.removeListener(this); - } - viewButtonMap.clear(); - } - - @Override - public void handleViewEvent(ViewEvent event) { - if (event instanceof DisplayNameChangedViewEvent - || event instanceof DescriptionChangedViewEvent) { - final Button btn = viewButtonMap.get(event.getSource()); - if (btn != null) { - getButtonFactory().updateButtonTexts(btn, event.getSource()); - } - } - } -} +/* + * Copyright (c) 2011 Petter Holmström + * + * 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 com.github.peholmst.mvp4vaadin.navigation.ui; + +import java.util.HashMap; +import java.util.Map; + +import com.github.peholmst.mvp4vaadin.View; +import com.github.peholmst.mvp4vaadin.ViewEvent; +import com.github.peholmst.mvp4vaadin.ViewListener; +import com.github.peholmst.mvp4vaadin.events.DescriptionChangedViewEvent; +import com.github.peholmst.mvp4vaadin.events.DisplayNameChangedViewEvent; +import com.github.peholmst.mvp4vaadin.navigation.NavigationController; +import com.github.peholmst.mvp4vaadin.navigation.NavigationControllerEvent; +import com.github.peholmst.mvp4vaadin.navigation.NavigationControllerListener; +import com.github.peholmst.mvp4vaadin.navigation.NavigationRequest; +import com.github.peholmst.mvp4vaadin.navigation.NavigationRequestBuilder; +import com.github.peholmst.mvp4vaadin.navigation.events.CurrentNavigationControllerViewChangedEvent; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.themes.BaseTheme; + +/** + * This class implements a breadcrumb navigation bar that shows all the views + * currently in a {@link NavigationController} as links in a row, where the + * first view corresponds to the first link, etc: + * + *

+ * First view >> Second view >> Third view >> ...
+ * 
+ *

+ * As the views change in the controller, the navigation bar will update itself. + * A click on any of the navigation links will request the view controller to + * navigate to that particular view. + *

+ * Both the links and the separators can be customized by implementing the + * {@link ButtonFactory} and {@link SeparatorFactory} interfaces, respectively. + * + * @see #setController(NavigationController) + * + * @author Petter Holmström + * @since 1.0 + */ +public class Breadcrumbs extends HorizontalLayout implements + NavigationControllerListener, ViewListener { + + private static final long serialVersionUID = 4513495936876605206L; + + public static final String BREADCRUMB_ELEMENT = "breadcrumb-element"; + + /** + * Factory interface for creating breadcrumb separators. + * + * @see Breadcrumbs#setSeparatorFactory(SeparatorFactory) + * @author Petter Holmström + * @since 1.0 + */ + public static interface SeparatorFactory extends java.io.Serializable { + /** + * Creates and returns a component to be used as a separator between + * breadcrumbs. + */ + Component createSeparator(); + } + + /** + * Default implementation of {@link SeparatorFactory}. The separators are + * labels containing the "»" character and having the + * {@link Breadcrumbs#BREADCRUMB_ELEMENT} style. + * + * @author Petter Holmström + * @since 1.0 + */ + public static class DefaultSeparatorFactory implements SeparatorFactory { + + private static final long serialVersionUID = 7957216244739746986L; + + @Override + public Component createSeparator() { + final Label separator = new Label("»"); + separator.setSizeUndefined(); + separator.addStyleName(BREADCRUMB_ELEMENT); + return separator; + } + } + + /** + * Factory interface for creating breadcrumb buttons. + * + * @see Breadcrumbs#setButtonFactory(ButtonFactory) + * @author Petter Holmström + * @since 1.0 + */ + public static interface ButtonFactory extends java.io.Serializable { + + /** + * Creates and returns a button for the specified view. The click + * listener will be registered by the breadcrumbs component. + */ + Button createButton(View view); + + /** + * Updates the button texts. This method is called when the display name + * and/or the description of the specified view are changed. + */ + void updateButtonTexts(Button button, View view); + } + + /** + * Default implementation of {@link ButtonFactory}. The created buttons have + * the {@link BaseTheme#BUTTON_LINK} and + * {@link Breadcrumbs#BREADCRUMB_ELEMENT} styles. + * + * @author Petter Holmström + * @since 1.0 + */ + public static class DefaultButtonFactory implements ButtonFactory { + + private static final long serialVersionUID = 8031407455065485896L; + + @Override + public Button createButton(View view) { + final Button btn = new Button(); + btn.setStyleName(BaseTheme.BUTTON_LINK); + btn.setSizeUndefined(); + btn.addStyleName(BREADCRUMB_ELEMENT); + updateButtonTexts(btn, view); + return btn; + } + + @Override + public void updateButtonTexts(Button button, View view) { + button.setCaption(view.getDisplayName()); + button.setDescription(view.getViewDescription()); + } + } + + private SeparatorFactory separatorFactory = new DefaultSeparatorFactory(); + + private ButtonFactory buttonFactory = new DefaultButtonFactory(); + + private NavigationController controller; + + private Map viewButtonMap = new HashMap(); + + /** + * Returns the navigation controller whose view stack will be displayed as + * breadcrumbs. If no controller has been set, null is + * returned. + */ + public NavigationController getController() { + return controller; + } + + /** + * Sets the navigation controller to use. This component will register + * itself as a listener of the controller. Setting the controller to + * null will unregister the listener. + */ + public void setController(NavigationController controller) { + if (this.controller != null) { + this.controller.removeListener(this); + } + this.controller = controller; + addBreadcrumbsForControllerRemovingAnyExistingOnes(); + if (this.controller != null) { + this.controller.addListener(this); + } + } + + /** + * Returns the separator factory to use for creating separators between + * breadcrumb buttons. + */ + public SeparatorFactory getSeparatorFactory() { + return separatorFactory; + } + + /** + * Sets the separator factory to use for creating separators between + * breadcrumb buttons. Set this value to null to use the + * default separator factory. + */ + public void setSeparatorFactory(SeparatorFactory separatorFactory) { + if (separatorFactory == null) { + separatorFactory = new DefaultSeparatorFactory(); + } + this.separatorFactory = separatorFactory; + } + + /** + * Returns the button factory to use for creating breadcrumb buttons. + */ + public ButtonFactory getButtonFactory() { + return buttonFactory; + } + + /** + * Sets the button factory to use for creating breadcrumb buttons. Set this + * value to null to use the default button factory. + */ + public void setButtonFactory(ButtonFactory buttonFactory) { + if (buttonFactory == null) { + buttonFactory = new DefaultButtonFactory(); + } + this.buttonFactory = buttonFactory; + } + + private void addBreadcrumbsForControllerRemovingAnyExistingOnes() { + removeBreadcrumbs(); + if (getController() != null) { + for (View view : getController().getViewStack()) { + addBreadcrumbForView(view); + } + } + } + + @Override + public void handleNavigationControllerEvent(NavigationControllerEvent event) { + if (event.getSource() != getController() + || !(event instanceof CurrentNavigationControllerViewChangedEvent)) { + return; + } + // TODO Maybe this method should be optimized so that not all buttons + // need to be removed unless absolutely necessary. + addBreadcrumbsForControllerRemovingAnyExistingOnes(); + } + + protected void addBreadcrumbForView(final View view) { + addSeparatorForView(view); + final Button btn = getButtonFactory().createButton(view); + final NavigationRequest navigationRequest = NavigationRequestBuilder + .newInstance().startWithPathToView(getController(), view) + .buildRequest(); + btn.addListener(new Button.ClickListener() { + + private static final long serialVersionUID = 5199653237630939848L; + + @Override + public void buttonClick(ClickEvent event) { + getController().navigate(navigationRequest); + } + }); + viewButtonMap.put(view, btn); + view.addListener(this); + addComponent(btn); + setComponentAlignment(btn, Alignment.MIDDLE_LEFT); + } + + protected void addSeparatorForView(final View view) { + if (getController().containsMoreThanOneElement() && !getController().getFirstView().equals(view)) { + Component separator = getSeparatorFactory().createSeparator(); + addComponent(separator); + setComponentAlignment(separator, Alignment.MIDDLE_LEFT); + } + } + + protected void removeBreadcrumbs() { + removeAllComponents(); + for (View view : viewButtonMap.keySet()) { + view.removeListener(this); + } + viewButtonMap.clear(); + } + + @Override + public void handleViewEvent(ViewEvent event) { + if (event instanceof DisplayNameChangedViewEvent + || event instanceof DescriptionChangedViewEvent) { + final Button btn = viewButtonMap.get(event.getSource()); + if (btn != null) { + getButtonFactory().updateButtonTexts(btn, event.getSource()); + } + } + } +}