// Copyright 2012 Google Inc. All Rights Reserved. // // 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.google.collide.client.search.awesomebox; import com.google.collide.client.search.awesomebox.AwesomeBox.AwesomeBoxSection; import com.google.collide.client.search.awesomebox.AwesomeBox.SectionIterationCallback; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.util.ListenerManager; import com.google.collide.shared.util.ListenerManager.Dispatcher; /** * The underlying AwesomeBox model used accross all AwesomeBox instances. */ // TODO: Provide push/pop context functionality so its much easier to go // into a temporarily restricted context with limited actions. public class AwesomeBoxModel { /** * Called when the context has been updated in the AwesomeBox model. */ public interface ContextChangeListener { /** * @param contextAlreadyActive true if * {@link AwesomeBoxModel#changeContext(AwesomeBoxContext)} was * called with a context which is already active. This case is just * for informational purposes and no changes are actually performed. */ public void onContextChanged(boolean contextAlreadyActive); } /** * Modes which change the behavior of getSelection. */ public enum SelectMode { /** * Returns null if there is no selection. */ DEFAULT, /** * Will attempt to select the first selectable item in the drop-down if * there isn't currently a selection. */ TRY_AUTOSELECT_FIRST_ITEM } /** * Modes which change the hide behavior of the dialog. */ public enum HideMode { /** * The component will autohide when the user clicks outside of the * AwesomeBox container or the actual input loses focus. */ AUTOHIDE, /** * The component will autohide only if user clicks outside of the AwesomeBox * container. This allows the AwesomeBox input to be hidden but the popup to * stay visible. */ DONT_HIDE_ON_INPUT_LOSE_FOCUS, /** * The component must be manually closed or programatically closed. */ NO_AUTOHIDE, } private AwesomeBoxContext currentContext; private AwesomeBoxSection selectedSection; private final ListenerManager<ContextChangeListener> listener; public AwesomeBoxModel() { currentContext = AwesomeBoxContext.DEFAULT; listener = ListenerManager.create(); } /** * Retrieves the current AwesomeBox autohide behavior. */ public HideMode getHideMode() { return currentContext.getHideMode(); } /** * Attempts to change the selection to the new section, if the section refuses * it will return false. If the section accepts selection any old selection * will be cleared. In the special case where the section is already selected * it will clear the selection and select the first or last item depending on * selectFirstItem. * * @param selectFirstItem True to select the first item, false to select the * last. * * @return true if the selection is set to the new section. */ boolean trySetSelection(AwesomeBoxSection section, boolean selectFirstItem) { if (selectedSection == section) { selectedSection.onClearSelection(); selectedSection.onMoveSelection(selectFirstItem); } else if (section.onMoveSelection(selectFirstItem)) { if (selectedSection != null) { selectedSection.onClearSelection(); } selectedSection = section; } else { return false; } return true; } /** * Retrieves the currently selected section. * * @return null if there is no selection. */ AwesomeBoxSection getSelection(SelectMode mode) { if (selectedSection == null && mode == SelectMode.TRY_AUTOSELECT_FIRST_ITEM) { selectFirstItem(); } return selectedSection; } /** * Updates the model selection. Without checking if the section will accept * selection. TrySetSelection should be preferred if you can't be sure the * section will accept selection. */ void setSelection(AwesomeBoxSection section) { if (selectedSection != section && selectedSection != null) { selectedSection.onClearSelection(); } selectedSection = section; } void clearSelection() { if (selectedSection != null) { selectedSection.onClearSelection(); } selectedSection = null; } /** * Will iterate through the sections until it finds the first section which * will accept selection and returns it. * * @return null if there are no no sections or no section accepts selection. */ AwesomeBoxSection selectFirstItem() { if (selectedSection != null) { selectedSection.onClearSelection(); } JsonArray<AwesomeBoxSection> sections = currentContext.getSections(); for (int i = 0; i < sections.size(); i++) { if (sections.get(i).onMoveSelection(true)) { selectedSection = sections.get(i); return sections.get(i); } } return null; } public ListenerManager<ContextChangeListener> getContextChangeListener() { return listener; } /** * @return the current context of the AwesomeBox. */ public AwesomeBoxContext getContext() { return currentContext; } /** * Changes to the specified context. */ public void changeContext(AwesomeBoxContext context) { if (currentContext == context) { listener.dispatch(new Dispatcher<ContextChangeListener>() { @Override public void dispatch(ContextChangeListener listener) { listener.onContextChanged(true); } }); return; } clearSelection(); currentContext = context; // Notify contexts of change event JsonArray<AwesomeBoxSection> sections = context.getSections(); for (int i = 0; i < sections.size(); i++) { sections.get(i).onContextChanged(currentContext); } // Dispatch the onContextChanged event listener.dispatch(new Dispatcher<ContextChangeListener>() { @Override public void dispatch(ContextChangeListener listener) { listener.onContextChanged(false); } }); } /** * @return The sections in the current context. */ public JsonArray<AwesomeBoxSection> getCurrentSections() { return currentContext.getSections(); } /** * Iterates through the section list starting at the specified section * (exclusive). This is a helper function that simplifies finding the next or * previous section. * * @param startSection Section to start iterating from (exclusive). * @param forward Direction to iterate. * @param sectionIterationCallback Callback to call for each iteration. */ void iterateFrom(AwesomeBoxSection startSection, boolean forward, SectionIterationCallback sectionIterationCallback) { JsonArray<AwesomeBoxSection> sections = currentContext.getSections(); for (int i = 0; i < sections.size(); i++) { if (startSection == sections.get(i)) { if (forward) { iterateForward(i + 1, sectionIterationCallback); } else { iterateBackwards(i - 1, sectionIterationCallback); } return; } } } /** * Iterates the section list starting at the given index moving backwards to * the beginning. */ private void iterateBackwards(int index, SectionIterationCallback sectionIterationCallback) { JsonArray<AwesomeBoxSection> sections = currentContext.getSections(); for (int i = index; i >= 0; i--) { if (!sectionIterationCallback.onIteration(sections.get(i))) { return; } } } /** * Iterates the section list starting at the given index and moving forward to * the end. */ private void iterateForward(int index, SectionIterationCallback sectionIterationCallback) { JsonArray<AwesomeBoxSection> sections = currentContext.getSections(); for (int i = index; i < sections.size(); i++) { if (!sectionIterationCallback.onIteration(sections.get(i))) { return; } } } }