blob: d8e95b96d7dc5860dc4c54b84c4546a5eca47443 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.swing.ui;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterators;
import org.apache.http.entity.ContentType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.JEditorPane;
import javax.swing.SwingUtilities;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.html.HTMLDocument;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A component that displays breadcrumbs to allow navigation.
*/
public class NavigationComponent<T extends NavigationComponent.Item> extends JEditorPane {
private boolean myDisplaySingleRoot;
/**
* Base class for the Breadcrumbs
*/
public static abstract class Item {
@NotNull
public abstract String getDisplayText();
}
/**
* Listener to receive notifications when items are selected.
*/
public interface ItemListener<T extends NavigationComponent.Item> {
void itemSelected(@NotNull T item);
}
private final ArrayList<ItemListener<T>> myItemListeners = new ArrayList<ItemListener<T>>();
private final LinkedList<T> myItemStack = new LinkedList<T>();
private boolean hasRootItem = false;
public NavigationComponent() {
setEditable(false);
setContentType(ContentType.TEXT_HTML.getMimeType());
putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
// Disable links decoration.
((HTMLDocument)getDocument()).getStyleSheet().addRule("a { text-decoration:none; }");
addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
return;
}
int idx = Integer.parseInt(e.getDescription());
final T item = myItemStack.get(idx);
for (final ItemListener<T> listener : myItemListeners) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
listener.itemSelected(item);
}
});
}
}
});
}
public void addItemListener(@NotNull ItemListener<T> itemListener) {
myItemListeners.add(itemListener);
}
public void removeItemListener(@NotNull ItemListener<T> itemListener) {
myItemListeners.remove(itemListener);
}
private void updateText() {
if (myItemStack.isEmpty()) {
setText("");
return;
}
if (myItemStack.size() == 1 && hasRootItem && !myDisplaySingleRoot) {
setText("");
return;
}
final AtomicInteger id = new AtomicInteger(myItemStack.size() - 1);
String text = Joiner.on(" &gt; ").join(Iterators.transform(myItemStack.descendingIterator(), new Function<T, String>() {
@Override
public String apply(T input) {
// Do not display link for the last element.
if (id.get() == 0) {
return input.getDisplayText();
}
return String.format("<a href=\"%d\">%s</a>", id.getAndDecrement(), input.getDisplayText());
}
@Override
public boolean equals(Object object) {
return false;
}
}));
setText(text);
}
@Override
public Dimension getMinimumSize() {
if (isMinimumSizeSet()) {
return super.getMinimumSize();
}
// The height of the JEditorPane on MacOS doesn't not update correctly when setting text if the height was previously 0.
// This makes sure that we use our own calculated minimum size in that situation so it works in every platform.
boolean hasContentToDisplay = myDisplaySingleRoot ? hasRootItem : myItemStack.size() > 1;
return hasContentToDisplay ? new Dimension(0, getFontMetrics(getFont()).getHeight()) : super.getMinimumSize();
}
/**
* Sets whether the component should display the root component if it's the only one in the navigation stack.
*/
public void setDisplaySingleRoot(boolean displaySingleRoot) {
myDisplaySingleRoot = displaySingleRoot;
}
/**
* Sets a root item that is always present in the navigation stack. The root item cannot be removed by {@link #pop}.
* @param item the root item value or null to remove the root.
*/
public void setRootItem(@Nullable T item) {
if (hasRootItem) {
myItemStack.removeFirst();
}
hasRootItem = item != null;
myItemStack.addFirst(item);
updateText();
}
/**
* Adds an element to the navigation stack.
*/
public void push(@NotNull T item) {
if (item.equals(peek())) {
return;
}
myItemStack.push(item);
updateText();
}
/**
* Removes an element from the navigation stack.
*/
@Nullable
public T pop() {
if (myItemStack.size() == 1 && hasRootItem) {
// The root item can not be removed
return null;
}
T removed = myItemStack.pop();
updateText();
return removed;
}
/**
* Returns the current element at the top of the navigation stack.
*/
@Nullable
public T peek() {
return myItemStack.peek();
}
/**
* Unwinds the navigation stack until the passed item is found. If the item is not found the whole stack will be cleared with the
* exception of the root element.
*/
public void goTo(@NotNull T goToItem) {
T item;
while ((item = peek()) != null) {
if (goToItem.equals(item)) {
return;
}
if (pop() == null) {
// End of the list.
return;
}
}
}
@Override
public Dimension getMaximumSize() {
if (isMaximumSizeSet()) {
return super.getMaximumSize();
}
// By default, allow only one line.
return new Dimension(Integer.MAX_VALUE, getFontMetrics(getFont()).getHeight());
}
}