/*
* 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;
}
}