blob: 037ecae3d799d06287e229871bc89caf74098ce6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.favorites;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.ObjectStreamException;
import java.util.Collections;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ActionMap;
import javax.swing.JFileChooser;
import javax.swing.text.DefaultEditorKit;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.StatusDisplayer;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.explorer.view.BeanTreeView;
import org.openide.explorer.view.TreeView;
import org.openide.explorer.view.Visualizer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.loaders.DataShadow;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeListener;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeOp;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
/**
* Physical tree view showing list of favorites.
*/
@TopComponent.Description(preferredID="favorites", persistenceType=TopComponent.PERSISTENCE_ALWAYS, iconBase="org/netbeans/modules/favorites/resources/actionView.png")
public class Tab extends TopComponent
implements Runnable, ExplorerManager.Provider {
static final long serialVersionUID =-8178367548546385799L;
static final RequestProcessor RP = new RequestProcessor("Favorites", 1); //NOI18N
static final String HELP_ID = Tab.class.getName();
private static final Logger LOG = Logger.getLogger(Tab.class.getName());
/* private */ static transient Tab DEFAULT; // package-private for unit tests
/** composited view */
protected transient TreeView view;
/** listeners to the root context and IDE settings */
private transient PropertyChangeListener weakRcL;
private transient NodeListener weakNRcL;
private transient NodeListener rcListener;
/** validity flag */
private transient boolean valid = true;
private ExplorerManager manager;
/** Creates */
private Tab() {
this.manager = new ExplorerManager();
ActionMap map = this.getActionMap ();
map.put(DefaultEditorKit.copyAction, ExplorerUtils.actionCopy(manager));
map.put(DefaultEditorKit.cutAction, ExplorerUtils.actionCut(manager));
map.put(DefaultEditorKit.pasteAction, ExplorerUtils.actionPaste(manager));
map.put("delete", ExplorerUtils.actionDelete (manager, true)); // or false
// following line tells the top component which lookup should be associated with it
associateLookup (ExplorerUtils.createLookup (manager, map));
}
@Override
public HelpCtx getHelpCtx () {
return new HelpCtx(Tab.HELP_ID);
}
@Override
public ExplorerManager getExplorerManager() {
return manager;
}
/** Initialize visual content of component */
@Override
protected void componentShowing () {
super.componentShowing ();
if (view == null) {
view = initGui ();
view.getAccessibleContext().setAccessibleName(
NbBundle.getMessage(
Tab.class, "ACSN_ExplorerBeanTree"));
view.getAccessibleContext().setAccessibleDescription(
NbBundle.getMessage(
Tab.class, "ACSD_ExplorerBeanTree"));
}
run();
}
/** Initializes gui of this component. Subclasses can override
* this method to install their own gui.
* @return Tree view that will serve as main view for this explorer.
*/
protected TreeView initGui () {
TreeView tView = new MyBeanTreeView();
tView.setRootVisible(false);
tView.setDragSource (true);
setLayout(new BorderLayout());
add (tView);
return tView;
}
@Override
protected void componentActivated() {
ExplorerUtils.activateActions(manager, true);
}
@Override
protected void componentDeactivated() {
ExplorerUtils.activateActions(manager, false);
}
/** Transfer focus to view. */
@Override
public void requestFocus () {
super.requestFocus();
if (view != null) {
view.requestFocus();
}
}
/** Transfer focus to view. */
@Override
public boolean requestFocusInWindow () {
super.requestFocusInWindow();
if (view != null) {
return view.requestFocusInWindow();
} else {
return false;
}
}
/** Sets new root context to view. Name, tooltip
* of this top component will be updated properly */
public void setRootContext (Node rc) {
Node oldRC = getExplorerManager().getRootContext();
// remove old listener, if possible
if (weakRcL != null) {
oldRC.removePropertyChangeListener(weakRcL);
}
if (weakNRcL != null) {
oldRC.removeNodeListener(weakNRcL);
}
getExplorerManager().setRootContext(rc);
initializeWithRootContext(rc);
}
public Node getRootContext () {
return getExplorerManager().getRootContext();
}
/** Implementation of DeferredPerformer.DeferredCommand
* Performs initialization of component's attributes
* after deserialization (component's name, etc,
* according to the root context) */
@Override
public void run() {
if (!valid) {
valid = true;
validateRootContext();
}
}
// Bugfix #5891 04 Sep 2001 by Jiri Rechtacek
// the title is derived from the root context
// it isn't changed by a selected node in the tree
/** Called when the explored context changes.
* Overriden - we don't want title to change in this style.
*/
protected void updateTitle () {
// set name by the root context
setName(getExplorerManager ().getRootContext().getDisplayName());
}
private NodeListener rcListener () {
if (rcListener == null) {
rcListener = new RootContextListener();
}
return rcListener;
}
/** Initialize this top component properly with information
* obtained from specified root context node */
private void initializeWithRootContext (Node rc) {
// update TC's attributes
setToolTipText(rc.getDisplayName());
setName(rc.getDisplayName());
updateTitle();
// attach listener
if (weakRcL == null) {
weakRcL = WeakListeners.propertyChange(
rcListener(), rc
);
}
rc.addPropertyChangeListener(weakRcL);
if (weakNRcL == null) {
weakNRcL = NodeOp.weakNodeListener (
rcListener(), rc
);
}
rc.addNodeListener(weakNRcL);
}
// put a request for later validation
// we must do this here, because of ExplorerManager's deserialization.
// Root context of ExplorerManager is validated AFTER all other
// deserialization, so we must wait for it
protected final void scheduleValidation() {
valid = false;
// initialize
RP.post(this, 100);
}
/* Updated accessible name of the tree view */
@Override
public void setName(String name) {
super.setName(name);
if (view != null) {
view.getAccessibleContext().setAccessibleName(name);
}
}
/* Updated accessible description of the tree view */
@Override
public void setToolTipText(String text) {
super.setToolTipText(text);
if (view != null) {
view.getAccessibleContext().setAccessibleDescription(text);
}
}
/** Multi - purpose listener, listens to: <br>
* 1) Changes of name, short description of root context.
* 2) Changes of IDE settings, namely delete confirmation settings */
private final class RootContextListener implements NodeListener {
@Override
public void propertyChange (PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
Object source = evt.getSource();
// root context node change
Node n = (Node)source;
if (Node.PROP_DISPLAY_NAME.equals(propName) ||
Node.PROP_NAME.equals(propName)) {
setName(n.getDisplayName());
} else if (Node.PROP_SHORT_DESCRIPTION.equals(propName)) {
setToolTipText(n.getShortDescription());
}
}
@Override
public void nodeDestroyed(NodeEvent nodeEvent) {
//Tab.this.setCloseOperation(TopComponent.CLOSE_EACH);
Tab.this.close();
}
@Override
public void childrenRemoved(NodeMemberEvent e) {}
@Override
public void childrenReordered(NodeReorderEvent e) {}
@Override
public void childrenAdded(NodeMemberEvent e) {}
} // end of RootContextListener inner class
/** Gets default instance. Don't use directly, it reserved for deserialization routines only,
* e.g. '.settings' file in xml layer, otherwise you can get non-deserialized instance. */
@Registration(mode="explorer", openAtStartup=false, position=300)
public static synchronized Tab getDefault() {
if (DEFAULT == null) {
DEFAULT = new Tab();
// put a request for later validation
// we must do this here, because of ExplorerManager's deserialization.
// Root context of ExplorerManager is validated AFTER all other
// deserialization, so we must wait for it
DEFAULT.scheduleValidation();
}
return DEFAULT;
}
/** Finds default instance. Use in client code instead of {@link #getDefault()}. */
@ActionID(id = "org.netbeans.modules.favorites.View", category = "Window")
@OpenActionRegistration(displayName="#ACT_View")
@ActionReference(position = 400, path = "Menu/Window")
public static synchronized Tab findDefault() {
if(DEFAULT == null) {
TopComponent tc = WindowManager.getDefault().findTopComponent("favorites"); // NOI18N
if(DEFAULT == null) {
Logger.getLogger(Tab.class.getName()).log(Level.WARNING, null,
new IllegalStateException("Can not find project component for its ID. Returned " +
tc)); // NOI18N
DEFAULT = new Tab();
// XXX Look into getDefault method.
DEFAULT.scheduleValidation();
}
}
return DEFAULT;
}
// ---- private implementation
/** Finds a node for given data object.
*/
private static Node findClosestNode (DataObject obj, Node start, boolean useLogicalViews) {
DataObject original = obj;
Stack<DataObject> stack = new Stack<> ();
while (obj != null) {
stack.push(obj);
DataObject tmp = obj.getFolder();
if (tmp == null) {
//Skip archive file root and do not stop at archive file root
//ie. continue to root of filesystem.
FileObject fo = FileUtil.getArchiveFile(obj.getPrimaryFile());
if (fo != null) {
try {
obj = DataObject.find(fo);
//Remove archive root from stack
stack.pop();
} catch (DataObjectNotFoundException exc) {
obj = null;
}
} else {
obj = null;
}
} else {
obj = tmp;
}
}
Node current = start;
int topIdx = stack.size();
int i = 0;
while (i < topIdx) {
Node n = findDataObject (current, stack.get (i));
if (n != null) {
current = n;
topIdx = i;
i = 0;
} else {
i++;
}
}
if (!check(current, original) && useLogicalViews) {
Node[] children = current.getChildren().getNodes();
for (Node child : children) {
Node n = selectInLogicalViews(original, child);
if (check(n, original)) {
current = n;
break;
}
}
}
return current;
}
private static Node selectInLogicalViews(DataObject original, Node start) {
return start;
/* Nothing for now.
DataShadow shadow = (DataShadow)start.getCookie(DataShadow.class);
if (shadow == null) {
return findClosestNode(original, start, false);
}
return start;
*/
}
/** Selects a node for given data object.
*/
private boolean selectNode (DataObject obj, Node root) {
Node node = findClosestNode(obj, root, true);
try {
getExplorerManager ().setSelectedNodes (new Node[] { node });
} catch (PropertyVetoException e) {
// you are out of luck!
throw new IllegalStateException();
}
return check(node, obj);
}
private static boolean check(Node node, DataObject obj) {
DataObject dObj = node.getLookup().lookup(DataObject.class);
if (obj == dObj) {
return true;
}
if (dObj instanceof DataShadow && obj == ((DataShadow)dObj).getOriginal()) {
return true;
}
return false;
}
/** Finds a data object among children of given node.
* @param node the node to search in
* @param obj the object to look for
*/
private static Node findDataObject (Node node, DataObject obj) {
Node[] arr = node.getChildren ().getNodes (true);
for (Node arr1 : arr) {
DataShadow ds = arr1.getCookie(DataShadow.class);
if ((ds != null) && (obj == ds.getOriginal())) {
return arr1;
} else {
DataObject o = arr1.getCookie(DataObject.class);
if ((o != null) && (obj == o)) {
return arr1;
}
}
}
return null;
}
/** Exchanges deserialized root context to projects root context
* to keep the uniquennes. */
protected void validateRootContext () {
EventQueue.invokeLater(() -> {
Node n = new AbstractNode(Children.LEAF);
n.setName(NbBundle.getMessage(Tab.class, "MSG_Tab.rootNode.loading")); //NOI18N
setRootContext(n);
});
final Node projectsRc = FavoritesNode.getNode ();
EventQueue.invokeLater(() -> setRootContext(projectsRc));
}
protected boolean containsNode(DataObject obj) {
Node node = findClosestNode(obj, getExplorerManager ().getRootContext (), true);
return check(node, obj);
}
protected void doSelectNode (final DataObject obj) {
//#142155: For some selected nodes there is no corresponding dataobject
if (obj == null) {
return;
}
Node root = getExplorerManager().getRootContext();
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Tab.class,"MSG_SearchingForNode"));
final boolean selected = selectNode(obj, root);
EventQueue.invokeLater(() -> {
if (selected) {
open();
requestActive();
scrollToSelection();
StatusDisplayer.getDefault().setStatusText(""); // NOI18N
} else {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Tab.class,"MSG_NodeNotFound"));
FileObject file = chooseFileObject(obj.getPrimaryFile());
if (file == null) {
return;
}
open();
requestActive();
try {
final DataObject dobj = DataObject.find(file);
RP.post(() -> Actions.Add.addToFavorites(Collections.singletonList(dobj)));
} catch (DataObjectNotFoundException e) {
LOG.log(Level.WARNING, null, e);
}
}
});
}
/**
*
* @return FileObject or null if FileChooser dialog is cancelled
*/
private static FileObject chooseFileObject(FileObject file) {
FileObject retVal = null;
File chooserSelection = null;
JFileChooser chooser = new JFileChooser ();
chooser.setFileSelectionMode( JFileChooser.FILES_AND_DIRECTORIES );
chooser.setDialogTitle(NbBundle.getBundle(Actions.class).getString ("CTL_DialogTitle"));
chooser.setApproveButtonText(NbBundle.getBundle(Actions.class).getString ("CTL_ApproveButtonText"));
chooser.setSelectedFile(FileUtil.toFile(file));
int option = chooser.showOpenDialog( WindowManager.getDefault().getMainWindow() ); // Show the chooser
if ( option == JFileChooser.APPROVE_OPTION ) {
chooserSelection = chooser.getSelectedFile();
File selectedFile = FileUtil.normalizeFile(chooserSelection);
//Workaround for JDK bug #5075580 (filed also in IZ as #46882)
if (!selectedFile.exists()) {
if ((selectedFile.getParentFile() != null) && selectedFile.getParentFile().exists()) {
if (selectedFile.getName().equals(selectedFile.getParentFile().getName())) {
selectedFile = selectedFile.getParentFile();
}
}
}
//#50482: Check if selected file exists eg. user can enter any file name to text box.
if (!selectedFile.exists()) {
String message = NbBundle.getMessage(Actions.class,"ERR_FileDoesNotExist",selectedFile.getPath());
String title = NbBundle.getMessage(Actions.class,"ERR_FileDoesNotExistDlgTitle");
DialogDisplayer.getDefault().notify
(new NotifyDescriptor(message,title,NotifyDescriptor.DEFAULT_OPTION,
NotifyDescriptor.INFORMATION_MESSAGE, new Object[] { NotifyDescriptor.CLOSED_OPTION },
NotifyDescriptor.OK_OPTION));
} else {
retVal = FileUtil.toFileObject(selectedFile);
assert retVal != null;
}
}
return retVal;
}
/** Old (3.2) deserialization of the ProjectTab */
public Object readResolve() throws ObjectStreamException {
getDefault().scheduleValidation();
return getDefault();
}
private void scrollToSelection() {
final Node[] selection = getExplorerManager().getSelectedNodes();
if( null == selection || selection.length < 1 )
return;
if( view instanceof MyBeanTreeView ) {
((MyBeanTreeView)view).scrollNodeToVisible(selection[0]);
}
}
private static class MyBeanTreeView extends BeanTreeView {
private void scrollNodeToVisible( final Node n ) {
EventQueue.invokeLater(() -> {
TreeNode tn = Visualizer.findVisualizer(n);
if (tn == null) {
return;
}
TreeModel model = tree.getModel();
if (!(model instanceof DefaultTreeModel)) {
return;
}
TreePath path = new TreePath(((DefaultTreeModel) model).getPathToRoot(tn));
if( null == path )
return;
Rectangle r = tree.getPathBounds(path);
if (r != null) {
tree.scrollRectToVisible(r);
}
});
}
}
} // end of Tab inner class