/* * Copyright 2003-2016 JetBrains s.r.o. * * 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 jetbrains.mps.workbench.choose; import com.intellij.ide.util.gotoByName.ChooseByNameModel; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.NavigationItem; import jetbrains.mps.ide.IdeBundle; import jetbrains.mps.util.NameUtil; import jetbrains.mps.util.Reference; import jetbrains.mps.util.containers.MultiMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.ListCellRenderer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; /** * Data model for a chooser that picks elements by name. Supports two 'scopes' of elements to pick from, 'local' and 'global' that may * correspond e.g. to project and application or locally-visible vs visible in the project elements. * <p/> * Replacement for {@link jetbrains.mps.workbench.choose.base.BaseMPSChooseModel}, with composition rather than * inheritance as usage pattern, and without need to know about peculiarities of {@link com.intellij.navigation.NavigationItem}, * {@link com.intellij.navigation.ItemPresentation} and correspondence of their methods to * methods of {@link ChooseByNameModel}, like {@link NavigationItem#getName()} vs {@link ChooseByNameModel#getElementName(Object)} * vs {@link ItemPresentation#getPresentableText()}. Besides, dumb mode control has nothing to do with choose model, and left to callers for management. * <p/> * Generally, there's no need to sub-class this class, composition should be enough, hence final. Nevertheless, the class is not inherently 'final', and * in case there's legitimate scenario that requires subclassing, 'final' can be removed. 'Legitimate', among other, means documented justification with * specific scenarios. * * XXX perhaps, it's worth adding own callback, parameterized, to avoid casts to <T> in Callback.elementChosen(). Alternative is to ressurect getModelObject * like in BaseMPSChooseModel (where it was needed because user objects were wrapped with NavigationItem) which would cast to <T>. I don't like unchecked * casts, though, that's why I don't fall into this alternative right away. * * @param <T> elements of the list to choose from. No restriction (e.g. like need for {@link Object#hashCode()} is imposed on elements. * * @author Artem Tikhomirov * @since 3.4 */ public final class ChooseByNameData<T> implements ChooseByNameModel { private final ElementPresentation<T> myPresentation; private String myCheckboxName; private String myNotInScopeMessage; private String myNotFoundMessage; private String myPromptText; private MultiMap<String, T> myLocalNameElementMap; private MultiMap<String, T> myGlobalNameElementMap; private boolean myInitialCheckboxState = false; private boolean myOpensEditor = false; private Iterable<T> myLocalScope; private Iterable<T> myGlobalScope; public ChooseByNameData(@NotNull ElementPresentation<T> presentation) { myPresentation = presentation; myLocalScope = myGlobalScope = Collections.emptyList(); } // ChooseByNameModel interface methods @Override public String getPromptText() { return myPromptText; } @Override public String getNotInMessage() { return myNotInScopeMessage; } @Override public String getNotFoundMessage() { return myNotFoundMessage; } @Nullable @Override public String getCheckBoxName() { return myCheckboxName; } //this is deprecated and not used @Override public char getCheckBoxMnemonic() { return 0; } @Override public boolean loadInitialCheckBoxState() { return myInitialCheckboxState; } /** * By default, keep the value in instance variable, override to persist elsewhere */ @Override public void saveInitialCheckBoxState(boolean state) { myInitialCheckboxState = state; } @Override public ListCellRenderer getListCellRenderer() { return new ChooseByNameRenderer(myPresentation); } @NotNull @Override public String[] getNames(boolean checkBoxState) { MultiMap<String, T> elementMap = checkBoxState ? myGlobalNameElementMap : myLocalNameElementMap; if (elementMap == null) { // ChooseByNameBase caches names, but we need to keep the map to ensure subsequent getElementsByName return elements with names we use in getNames(). // FIXME use of multimap is merely a quick way to support elements with identical names. It's quite ineffective (memory-wise) structure. elementMap = buildMap(getElements(checkBoxState)); if (checkBoxState) { myGlobalNameElementMap = elementMap; } else { myLocalNameElementMap = elementMap; } } // XXX no idea whether getNames() is expected to return unique values only. Provided getElementByName() takes single name and expects multiple values, // likely, unique names are expected here. It's ok with MultiMap, however needs attention if we switch to another container. Set<String> keys = elementMap.keySet(); return keys.toArray(new String[keys.size()]); } private MultiMap<String, T> buildMap(Iterable<T> elements) { MultiMap<String, T> rv = new MultiMap<String, T>() { @Override protected Collection<T> createCollection() { // I don't expect a lot of duplicating names return new ArrayList<>(4); } }; myPresentation.names(elements, (t, s) -> rv.putValue(s, t)); return rv; } @NotNull @Override public Object[] getElementsByName(String name, boolean checkBoxState, String pattern) { MultiMap<String, T> elementMap = checkBoxState ? myGlobalNameElementMap : myLocalNameElementMap; // not that I insist not to invoke getNames() when myNamesElementMap is empty, just curious if there's real scenario (not in test) when getElementByName comes first assert elementMap != null : "How come getElementsByName() is invoked before getNames()? Where from the name comes then?"; Collection<T> rv = elementMap.get(name); return rv.toArray(); } @Nullable @Override public String getElementName(Object element) { // there's subtle and undocumented distinction between getElementName and getFullName. Latter is used for statistics only. // former looks like expected to be short form of the full name and is utilized to detect best match together with statistics of full name. // There's no other indication but name of variable that elementName has to be 'short' (ChooseByNameBase.detectBestStatisticalPosition() // initializes String shortName = myModel.getElementName(modelElement)), and there's GotoActionModel that doesn't make any distinction // between element and full name, so I don't see a reason to implement getElementName and getFullName differently. // Neither I found any constraint for this name to match any from names(). @SuppressWarnings("unchecked") T e = (T) element; final Reference<String> rv = new Reference<>(); myPresentation.names(Collections.singleton(e), (t, s) -> rv.set(s)); return rv.get(); } @Nullable @Override public String getFullName(Object element) { return getElementName(element); } @NotNull @Override public String[] getSeparators() { return new String[]{"."}; } @Nullable @Override public String getHelpId() { return null; } @Override public boolean willOpenEditor() { return myOpensEditor; } @Override public boolean useMiddleMatching() { return true; // value from BaseMPSChooseModel } // Own stuff /** * Configure chooser. Invoke this method prior to use of the model. * @return {@code this} for convenience */ public ChooseByNameData<T> setInitialCheckboxState(boolean initialCheckboxState) { myInitialCheckboxState = initialCheckboxState; return this; } /** * Configure chooser. Invoke this method prior to use of the model. * @return {@code this} for convenience */ public ChooseByNameData<T> setOpensEditor(boolean opensEditor) { myOpensEditor = opensEditor; return this; } /** * @param checkboxName hint for check-box to switch between 'local' and 'global' scope, pass {@code null} to disable the switch. * @return {@code this} for convenience */ public ChooseByNameData<T> setCheckBoxName(@Nullable String checkboxName) { myCheckboxName = checkboxName; return this; } /** * Initialize different hint labels visible in popup. * Note, {@linkplain #getCheckBoxName() checkbox name} is not affected * @return {@code this} for convenience */ public ChooseByNameData<T> setPrompts(@NotNull String promptText, @NotNull String notFoundMessage, @NotNull String notInScopeMessage) { // I know ChooseByNameBase tolerates promptText == null, it doesn't hurt though to be more strict about names, we can relax this later, if needed. myPromptText = promptText; myNotInScopeMessage = notInScopeMessage; myNotFoundMessage = notFoundMessage; return this; } /** * Tribute to {@code BaseMPSChooseModel} constructor that composed prompts based on generic name of an element. * Note, in addition to values initialized with {@link #setPrompts(String, String, String)}, derives value for {@link #getCheckBoxName()} as well. * If you don't need checkbox, don't forget to {@link #setCheckBoxName(String)} to {@code null}. * @return {@code this} for convenience */ public ChooseByNameData<T> derivePrompts(String elementName) { myCheckboxName = String.format(IdeBundle.message("checkbox.include.non.project.elements"), NameUtil.pluralize(elementName)); myPromptText = String.format(IdeBundle.message("lable.elemet.name"), NameUtil.capitalize(elementName)); myNotInScopeMessage = String.format(IdeBundle.message("lable.no.elemet.found.in.scope"), NameUtil.pluralize(elementName)); myNotFoundMessage = com.intellij.ide.IdeBundle.message("label.choosebyname.no.matches.found"); return this; } /** * Configure chooser. Invoke this method prior to use of the model. * Perhaps, need an alternative constructor that would take scope as well. * <p/> * The choice of {@code Iterable} for scope is not a sure thing. I don't see a point for a custom interface here; {@code Supplier<Iterable<T>>} * looks bit too much, and {@code Iterable} seems quite handy with streams. * @param localScope default set of elements, available without extras/global staff made available with {@linkplain #getCheckBoxName() checkbox} * @param globalScope extended set of elements, available on explicit request from user. {@code null} indicates same scope as local shall be used. * This is done to facilitate in-place local scope creation (new) without a need to extract a local variable. * Though unlikely needed, explicit empty scope shall get passed if it's true to get nothing at global scope (any empty * collection would do). * @return {@code this} for convenience */ public final ChooseByNameData<T> setScope(@NotNull Iterable<T> localScope, @Nullable Iterable<T> globalScope) { myLocalScope = localScope; myGlobalScope = globalScope == null ? localScope : globalScope; return this; } protected Iterable<T> getElements(boolean isGlobalScope) { return isGlobalScope ? myGlobalScope : myLocalScope; } }