package org.esa.snap.netbeans.docwin;
import org.openide.modules.OnStart;
import org.openide.util.Lookup;
import org.openide.windows.Mode;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Manages global opening, closing, and selection of {@link DocumentWindow}s.
*
* @author Norman Fomferra
* @since 2.0
*/
public class DocumentWindowManager implements WindowContainer<DocumentWindow> {
private static DocumentWindowManager defaultInstance;
private final Map<Listener, Set<Predicate>> listeners;
private final Set<DocumentWindow> openDocumentWindows;
private DocumentWindow selectedDocumentWindow;
/**
* Gets the {@code DocumentWindowManager}'s default implementation. It does this by
* <pre>
* DocumentWindowManager instance = Lookup.getDefault().lookup(DocumentWindowManager.class);
* return (instance != null) ? instance : getDefaultInstance();
* </pre>
*
* @return the default implementation
*/
public static DocumentWindowManager getDefault() {
DocumentWindowManager instance = Lookup.getDefault().lookup(DocumentWindowManager.class);
return (instance != null) ? instance : getDefaultInstance();
}
/**
* Constructor.
*/
protected DocumentWindowManager() {
listeners = new LinkedHashMap<>();
openDocumentWindows = new LinkedHashSet<>();
captureCurrentState();
WindowManager.getDefault().getRegistry().addPropertyChangeListener(new RegistryPropertyChangeDelegate());
}
/**
* Gets the currently selected document windows.
*
* @return the currently selected document windows. May be {@code null}.
*/
@Override
public DocumentWindow getSelectedWindow() {
return selectedDocumentWindow;
}
/**
* Gets all opened document windows.
*
* @return All opened document windows. List may be empty.
*/
@Override
public List<DocumentWindow> getOpenedWindows() {
return new ArrayList<>(openDocumentWindows);
}
/**
* Opens a document window.
* <p>
* Document windows are initially opened in the NetBeans {@code "editor"} mode which
* is the central panel of the main frame.
*
* @param documentWindow The document window to be opened.
* @return {@code true} on success
*/
public boolean openWindow(DocumentWindow documentWindow) {
TopComponent topComponent = documentWindow.getTopComponent();
WorkspaceTopComponent workspaceTopComponent = WorkspaceTopComponent.findShowingInstance();
if (workspaceTopComponent != null) {
workspaceTopComponent.addTopComponent(topComponent);
return true;
}
Mode editor = WindowManager.getDefault().findMode("editor");
if (editor.dockInto(topComponent)) {
topComponent.open();
return true;
}
return false;
}
/**
* Closes a document window.
*
* @param documentWindow The document window to be closed.
* @return {@code true} on success
*/
public boolean closeWindow(DocumentWindow documentWindow) {
Optional<WorkspaceTopComponent> anyWorkspace = WindowUtilities
.getOpened(WorkspaceTopComponent.class)
.filter(tc -> tc.getTopComponents().contains(documentWindow.getTopComponent())).findAny();
if (anyWorkspace.isPresent()) {
return anyWorkspace.get().removeTopComponent(documentWindow.getTopComponent());
} else {
return removeOpenedWindow(documentWindow);
}
}
/**
* Requests that the given document window is being selected.
*
* @param documentWindow The document window to be selected.
*/
public void requestSelected(DocumentWindow documentWindow) {
TopComponent topComponent = documentWindow.getTopComponent();
List<WorkspaceTopComponent> showingWorkspaces = WorkspaceTopComponent.findShowingInstances();
for (WorkspaceTopComponent showingWorkspace : showingWorkspaces) {
if (showingWorkspace.getTopComponents().contains(topComponent)) {
showingWorkspace.requestActiveTopComponent(topComponent);
return;
}
}
topComponent.requestActive();
}
/**
* Adds a document window listener for any document type.
*
* @param listener The listener.
*/
public final void addListener(Listener listener) {
//noinspection unchecked
addListener(Predicate.any(), listener);
}
/**
* Adds a document window listener for the given document (base) type.
*
* @param predicate The window selector.
* @param listener The listener.
*/
public final <D, V> void addListener(Predicate<D, V> predicate, Listener<D, V> listener) {
Set<Predicate> predicates = listeners.get(listener);
if (predicates == null) {
predicates = new HashSet<>();
listeners.put(listener, predicates);
}
predicates.add(predicate);
}
/**
* Removes the document window listener registered for any document type.
*
* @param listener The listener.
*/
public final void removeListener(Listener listener) {
listeners.remove(listener);
}
/**
* Removes the document window listener registered for the given window selector.
*
* @param listener The listener.
*/
public final <D, V> void removeListener(Predicate<D, V> predicate, Listener<D, V> listener) {
Set<Predicate> predicates = listeners.get(listener);
if (predicates != null) {
predicates.remove(predicate);
if (predicates.isEmpty()) {
listeners.remove(listener);
}
}
}
/**
* Gets all registered document window listeners.
*
* @return The array of listeners which may be empty.
*/
public final Listener[] getListeners() {
return listeners.keySet().toArray(new Listener[listeners.size()]);
}
/**
* Gets the document window listeners registered for the given window selector.
*
* @param documentWindow The document window.
* @return The array of listeners which may be empty.
*/
final Listener[] getListeners(DocumentWindow documentWindow) {
ArrayList<Listener> listeners = new ArrayList<>();
for (Map.Entry<Listener, Set<Predicate>> entry : this.listeners.entrySet()) {
Listener listener = entry.getKey();
Set<Predicate> predicates = entry.getValue();
for (Predicate predicate : predicates) {
if (isPredicateApplicable(predicate, documentWindow) && predicate.test(documentWindow)) {
listeners.add(listener);
break;
}
}
}
return listeners.toArray(new Listener[listeners.size()]);
}
private boolean isPredicateApplicable(Predicate predicate, DocumentWindow documentWindow) {
Object document = documentWindow.getDocument();
Object view = documentWindow.getView();
Class<?> actualDocType = document != null ? document.getClass() : Object.class;
Class<?> actualViewType = view != null ? view.getClass() : Object.class;
Class<?> requestedDocType = predicate.getDocType();
Class<?> requestedViewType = predicate.getViewType();
return requestedDocType.isAssignableFrom(actualDocType)
&& requestedViewType.isAssignableFrom(actualViewType);
}
void addOpenedWindow(DocumentWindow documentWindow) {
if (openDocumentWindows.add(documentWindow)) {
fireWindowEvent(Event.Type.WINDOW_OPENED, documentWindow);
}
}
boolean removeOpenedWindow(DocumentWindow documentWindow) {
if (openDocumentWindows.remove(documentWindow)) {
if (getSelectedWindow() == documentWindow) {
setSelectedWindow(null);
}
boolean isClosed = documentWindow.getTopComponent().close();
if (isClosed) {
fireWindowEvent(Event.Type.WINDOW_CLOSED, documentWindow);
}
return isClosed;
}
return false;
}
void setSelectedWindow(DocumentWindow newValue) {
DocumentWindow oldValue = this.selectedDocumentWindow;
if (oldValue != newValue) {
this.selectedDocumentWindow = newValue;
if (oldValue != null) {
oldValue.componentDeselected();
fireWindowEvent(Event.Type.WINDOW_DESELECTED, oldValue);
}
if (newValue != null) {
newValue.componentSelected();
fireWindowEvent(Event.Type.WINDOW_SELECTED, newValue);
}
}
}
private void fireWindowEvent(Event.Type eventType, DocumentWindow documentWindow) {
Listener[] listeners = getListeners(documentWindow);
if (listeners.length > 0) {
//noinspection unchecked
Event event = new Event(eventType, documentWindow);
for (Listener listener : listeners) {
switch (eventType) {
case WINDOW_OPENED:
//noinspection unchecked
listener.windowOpened(event);
break;
case WINDOW_CLOSED:
//noinspection unchecked
listener.windowClosed(event);
break;
case WINDOW_SELECTED:
//noinspection unchecked
listener.windowSelected(event);
break;
case WINDOW_DESELECTED:
//noinspection unchecked
listener.windowDeselected(event);
break;
}
}
}
}
private static DocumentWindowManager getDefaultInstance() {
if (defaultInstance == null) {
defaultInstance = new DocumentWindowManager();
}
return defaultInstance;
}
private void captureCurrentState() {
TopComponent.Registry registry = WindowManager.getDefault().getRegistry();
Set<TopComponent> topComponents = registry.getOpened();
topComponents.stream()
.filter(topComponent -> topComponent instanceof DocumentWindow)
.forEach(topComponent -> {
DocumentWindow documentWindow = (DocumentWindow) topComponent;
openDocumentWindows.add(documentWindow);
if (registry.getActivated() == topComponent) {
selectedDocumentWindow = documentWindow;
}
});
}
/**
* A predicate is used to select document windows.
*
* @param <D> The document type.
* @param <V> The view type.
*/
public interface Predicate<D, V> {
/**
* @return the requested document type.
*/
Class<D> getDocType();
/**
* @return the requested view type.
*/
Class<V> getViewType();
/**
* Tests if this predicate applies to the given document window.
* <p>
* The method will only be called if it has already been ensured that the {@link #getDocType() requested document type}
* is assignable from the type of the
* window's {@link DocumentWindow#getDocument document}
* and that the {@link #getViewType() requested view type} is assignable from the type of the window's
* {@link DocumentWindow#getView view}.
*
* @param window The document window
* @return {@code true} if this predicate applies to the given document window.
*/
boolean test(DocumentWindow window);
/**
* @return A predicate that applies to any document window.
*/
static Predicate<Object, Object> any() {
return DefaultPredicate.ANY;
}
/**
* @return A predicate that applies to document windows with the given {@code docType}.
*/
static <D> Predicate<D, Object> doc(Class<D> docType) {
return new DefaultPredicate<>(docType, Object.class);
}
/**
* @return A predicate that applies to document windows with the given {@code viewType}.
*/
static <V> Predicate<Object, V> view(Class<V> viewType) {
return new DefaultPredicate<>(Object.class, viewType);
}
/**
* @return A predicate that applies to document windows with the given {@code docType} and {@code viewType}.
*/
static <D, V> Predicate<D, V> docView(Class<D> docType, Class<V> viewType) {
return new DefaultPredicate<>(docType, viewType);
}
}
/**
* Default {@link Predicate} implementation.
*
* @param <D> The document type.
* @param <V> The view type.
*/
public static class DefaultPredicate<D, V> implements Predicate<D, V> {
private static final Predicate<Object, Object> ANY = new DefaultPredicate<>(Object.class, Object.class);
private final Class<D> docType;
private final Class<V> viewType;
public DefaultPredicate(Class<D> docType, Class<V> viewType) {
this.docType = docType;
this.viewType = viewType;
}
@Override
public Class<D> getDocType() {
return docType;
}
@Override
public Class<V> getViewType() {
return viewType;
}
/**
* Always returns {@code true}. Override if you want a more detailed test.
*
* @param window The document window
* @return Always {@code true}.
*/
@Override
public boolean test(DocumentWindow window) {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DefaultPredicate<?, ?> that = (DefaultPredicate<?, ?>) o;
return getDocType().equals(that.getDocType()) && getViewType().equals(that.getViewType());
}
@Override
public int hashCode() {
int result = getDocType().hashCode();
result = 31 * result + getViewType().hashCode();
return result;
}
}
/**
* An {@code Event} that adds support for
* {@code DocumentWindow} objects as the event source.
*/
public static final class Event<D, V> extends EventObject {
/**
* The type of event.
*/
public enum Type {
WINDOW_OPENED,
WINDOW_CLOSED,
WINDOW_SELECTED,
WINDOW_DESELECTED
}
private final DocumentWindow<D, V> documentWindow;
private final Type type;
Event(Type type, DocumentWindow<D, V> documentWindow) {
super(documentWindow);
this.type = type;
this.documentWindow = documentWindow;
}
/**
* @return The type of event.
*/
public Type getType() {
return type;
}
/**
* @return The document window.
*/
public DocumentWindow<D, V> getWindow() {
return documentWindow;
}
@Override
public String toString() {
return "Event{" +
"type=" + type +
", documentWindow=" + documentWindow +
'}';
}
}
/**
* The listener interface for receiving document window events.
*/
public interface Listener<D, V> extends EventListener {
/**
* Invoked when a document window has been opened.
*/
default void windowOpened(Event<D, V> e) {
}
/**
* Invoked when a document window has been closed.
*/
default void windowClosed(Event<D, V> e) {
}
/**
* Invoked when a document window has been selected.
*/
default void windowSelected(Event<D, V> e) {
}
/**
* Invoked when a document window has been de-selected.
*/
default void windowDeselected(Event<D, V> e) {
}
}
/**
* This class is not API. It is public as an implementation detail.
* <p>
* Makes sure DocumentWindowManager can start listening to window events from the beginning.
*/
@SuppressWarnings("unused")
@OnStart
public static class Starter implements Runnable {
@Override
public void run() {
getDefault();
}
}
private class RegistryPropertyChangeDelegate implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (TopComponent.Registry.PROP_ACTIVATED.equals(evt.getPropertyName())) {
Object newValue = evt.getNewValue();
if (newValue instanceof DocumentWindow) {
setSelectedWindow((DocumentWindow) newValue);
}
} else if (TopComponent.Registry.PROP_TC_OPENED.equals(evt.getPropertyName())) {
Object newValue = evt.getNewValue();
if (newValue instanceof DocumentWindow) {
addOpenedWindow((DocumentWindow) newValue);
}
} else if (TopComponent.Registry.PROP_TC_CLOSED.equals(evt.getPropertyName())) {
Object newValue = evt.getNewValue();
if (newValue instanceof DocumentWindow) {
removeOpenedWindow((DocumentWindow) newValue);
}
}
}
}
}