--- a/api.search/src/org/netbeans/modules/search/MatchingObject.java +++ a/api.search/src/org/netbeans/modules/search/MatchingObject.java @@ -89,6 +89,7 @@ public static final String PROP_INVALIDITY_STATUS = "invalidityStatus"; //NOI18N public static final String PROP_SELECTED = "selected"; //NOI18N + public static final String PROP_REMOVED = "removed"; //NOI18N /** */ private static final Logger LOG = @@ -535,14 +536,15 @@ List detailNodes = new ArrayList(textDetails.size()); for (TextDetail txtDetail : textDetails) { - detailNodes.add(new TextDetail.DetailNode(txtDetail, false)); + detailNodes.add( + new TextDetail.DetailNode(txtDetail, false, resultModel)); } return detailNodes.toArray(new Node[detailNodes.size()]); } public Children getDetailsChildren(boolean replacing) { - return new DetailsChildren(replacing); + return new DetailsChildren(replacing, resultModel); } /** @@ -990,6 +992,14 @@ } /** + * Remove this matching object from its result model and inform listeners. + */ + public void remove() { + resultModel.remove(this); + changeSupport.firePropertyChange(PROP_REMOVED, null, null); + } + + /** * Bridge between new API and legacy implementation, will be deleted. */ public static class Def { @@ -1032,15 +1042,17 @@ private class DetailsChildren extends Children.Keys { private boolean replacing; + private final ResultModel model; - public DetailsChildren(boolean replacing) { + public DetailsChildren(boolean replacing, ResultModel model) { this.replacing = replacing; setKeys(getTextDetails()); + this.model = model; } @Override protected Node[] createNodes(TextDetail key) { - return new Node[]{new TextDetail.DetailNode(key, replacing)}; + return new Node[]{new TextDetail.DetailNode(key, replacing, model)}; } } --- a/api.search/src/org/netbeans/modules/search/ResultModel.java +++ a/api.search/src/org/netbeans/modules/search/ResultModel.java @@ -48,7 +48,10 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.netbeans.modules.search.Constants.Limit; @@ -64,7 +67,10 @@ * @author Marian Petras */ public final class ResultModel { - + private static final Logger LOG = + Logger.getLogger(ResultModel.class.getName()); + public static final String PROP_REMOVED = "remove"; //NOI18N + public static final String PROP_REMOVEDDETAIL = "removeDetail"; //NOI18N public static final String PROP_SELECTION = "selection"; //NOI18N public static final String PROP_VALID = "valid"; //NOI18N public static final String PROP_MATCHING_OBJECTS = @@ -115,6 +121,56 @@ isFullText = (basicCriteria != null) && basicCriteria.isFullText(); startTime = -1; } + + /** + * Remove the {@link MatchingObject} from the model and informs the + * listeners. + * + * @param mo Matching object to remove. + */ + public synchronized void remove(MatchingObject mo) { + if (matchingObjects.remove(mo)) { + totalDetailsCount -= getDetailsCount(mo); + int deselected = 0; + if (mo.getTextDetails() != null) { + for (TextDetail td : mo.getTextDetails()) { + deselected += td.isSelected() ? -1 : 0; + } + } + mo.cleanup(); + // inform listeners, old object contains removed object + propertyChangeSupport.firePropertyChange(PROP_REMOVED, + Arrays.asList(mo), null); + if (deselected < 0) { + updateSelected(deselected); + } + } + } + + public void removeDetailMatch(TextDetail txtDetail) { + MatchingObject found = getMatchingObjectFor(txtDetail); + LOG.log(Level.FINE, "Found {0}", found); //NOI18N + if (null != found && found.textDetails.remove(txtDetail)) { + totalDetailsCount--; + if (found.textDetails.isEmpty()) { + remove(found); + } else { + propertyChangeSupport.firePropertyChange(PROP_REMOVEDDETAIL, + Arrays.asList(txtDetail), null); + } + } + } + + private MatchingObject getMatchingObjectFor(TextDetail txtDetail) { + for (MatchingObject mo : matchingObjects) { + for (TextDetail textDetail : mo.getTextDetails()) { + if (textDetail == txtDetail) { + return mo; + } + } + } + return null; + } /** */ --- a/api.search/src/org/netbeans/modules/search/TextDetail.java +++ a/api.search/src/org/netbeans/modules/search/TextDetail.java @@ -50,6 +50,7 @@ import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.io.CharConversionException; +import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -451,6 +452,7 @@ /** Cached toString value. */ private String name; private String htmlDisplayName; + private final ResultModel model; /** * Constructs a node representing the specified information about @@ -458,11 +460,13 @@ * * @param txtDetail information to be represented by this node */ - public DetailNode(TextDetail txtDetail, boolean replacing) { + public DetailNode(TextDetail txtDetail, boolean replacing, + ResultModel model) { super(Children.LEAF, Lookups.fixed(txtDetail, new ReplaceCheckableNode(txtDetail, replacing))); this.txtDetail = txtDetail; + this.model = model; setValue(SearchDisplayer.ATTR_OUTPUT_LINE, DetailNode.getFullDesc(txtDetail)); @@ -475,7 +479,8 @@ @Override public void stateChanged(ChangeEvent e) { fireIconChange(); - ResultsOutlineSupport.toggleParentSelected(DetailNode.this); + ResultsOutlineSupport.toggleParentSelected( + DetailNode.this.getParentNode()); } }); setIconBaseWithExtension(ICON); @@ -823,6 +828,16 @@ protected void createPasteTypes(Transferable t, List s) { } + @Override + public boolean canDestroy() { + return true; + } + + @Override + public void destroy() throws IOException { + this.model.removeDetailMatch(txtDetail); +// super.destroy(); + } } // End of DetailNode class. /** --- a/api.search/src/org/netbeans/modules/search/ui/AbstractSearchResultsPanel.java +++ a/api.search/src/org/netbeans/modules/search/ui/AbstractSearchResultsPanel.java @@ -126,6 +126,12 @@ this.searchProviderPresenter = searchProviderPresenter; initComponents(); explorerManager = new ExplorerManager(); + + ActionMap map = this.getActionMap(); + // map delete key to delete action + map.put("delete", //NOI18N + ExplorerUtils.actionDelete(explorerManager, false)); + lookup = ExplorerUtils.createLookup(explorerManager, ResultView.getInstance().getActionMap()); initActions(); --- a/api.search/src/org/netbeans/modules/search/ui/BasicAbstractResultsPanel.java +++ a/api.search/src/org/netbeans/modules/search/ui/BasicAbstractResultsPanel.java @@ -45,6 +45,8 @@ import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.beans.PropertyVetoException; import java.util.ResourceBundle; import javax.accessibility.AccessibleContext; @@ -74,7 +76,7 @@ * @author jhavlin */ public abstract class BasicAbstractResultsPanel - extends AbstractSearchResultsPanel { + extends AbstractSearchResultsPanel implements PropertyChangeListener { @StaticResource private static final String SHOW_DETAILS_ICON = @@ -120,8 +122,15 @@ setRootDisplayName(NbBundle.getMessage(ResultView.class, "TEXT_SEARCHING___")); //NOI18N initAccessibility(); + this.resultModel.addPropertyChangeListener( + ResultModel.PROP_REMOVED, this); } + @Override + public void propertyChange (PropertyChangeEvent evt) { + // update the root node after change in model + setFinalRootNodeText(); + } public void update() { if (details && btnExpand.isVisible() && !btnExpand.isEnabled()) { --- a/api.search/src/org/netbeans/modules/search/ui/HideResultAction.java +++ a/api.search/src/org/netbeans/modules/search/ui/HideResultAction.java @@ -0,0 +1,79 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.netbeans.modules.search.ui; + +import java.awt.event.ActionEvent; +import org.openide.actions.DeleteAction; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; +import org.openide.util.actions.CallbackSystemAction; +import org.openide.util.actions.SystemAction; + +/** + * + * @author jhavlin + */ +@NbBundle.Messages({"HideResultAction.displayName=Hide"}) +public class HideResultAction extends CallbackSystemAction { + + CallbackSystemAction delegate = SystemAction.get(DeleteAction.class); + + @Override + public void actionPerformed(ActionEvent e) { + delegate.actionPerformed(e); + } + + @Override + public String getName() { + return Bundle.HideResultAction_displayName(); + } + + @Override + public HelpCtx getHelpCtx() { + return delegate.getHelpCtx(); + } + + @Override + public boolean isEnabled() { + return true; + } +} --- a/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java +++ a/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java @@ -59,6 +59,7 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.modules.search.MatchingObject; import org.netbeans.modules.search.MatchingObject.InvalidityStatus; +import org.openide.actions.DeleteAction; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.loaders.DataObject; @@ -103,8 +104,8 @@ PropertySet[] propertySets; public MatchingObjectNode(Node original, - org.openide.nodes.Children children, - MatchingObject matchingObject, final boolean replacing) { + org.openide.nodes.Children children, MatchingObject matchingObject, + final boolean replacing) { this(original, children, matchingObject, new ReplaceCheckableNode(matchingObject, replacing)); } @@ -130,8 +131,7 @@ MatchingObject.PROP_INVALIDITY_STATUS, validityListener); selectionListener = new SelectionListener(); - matchingObject.addPropertyChangeListener(MatchingObject.PROP_SELECTED, - selectionListener); + matchingObject.addPropertyChangeListener(selectionListener); } @Override @@ -165,7 +165,8 @@ if (!context) { return new Action[]{ SystemAction.get(OpenMatchingObjectsAction.class), - new CopyPathAction() + new CopyPathAction(), + SystemAction.get(HideResultAction.class) }; } else { return new Action[0]; @@ -234,7 +235,7 @@ @Override public boolean canDestroy() { - return false; + return true; } public void clean() { @@ -274,6 +275,12 @@ return propertySets; } + @Override + public void destroy () throws IOException { + // when removing the node, the node's content is removed from model + this.matchingObject.remove(); + } + /** * Check whether the file object is valid and a valid data object can be * found for it. It should be checked after original node is destroyed. It @@ -443,7 +450,7 @@ fireIconChange(); ResultsOutlineSupport.toggleParentSelected( - MatchingObjectNode.this); + MatchingObjectNode.this.getParentNode()); } } } --- a/api.search/src/org/netbeans/modules/search/ui/ResultsOutlineSupport.java +++ a/api.search/src/org/netbeans/modules/search/ui/ResultsOutlineSupport.java @@ -88,6 +88,7 @@ import org.openide.nodes.Node; import org.openide.util.Exceptions; import org.openide.util.ImageUtilities; +import org.openide.util.actions.SystemAction; import org.openide.util.datatransfer.PasteType; import org.openide.util.lookup.Lookups; @@ -125,7 +126,7 @@ this.details = details; this.resultModel = resultModel; this.basicComposition = basicComposition; - this.resultsNode = new ResultsNode(); + this.resultsNode = new ResultsNode(resultModel); this.infoNode = infoNode; this.invisibleRoot = new RootNode(resultsNode, infoNode); this.matchingObjectNodes = new LinkedList(); @@ -312,10 +313,11 @@ private FolderTreeChildren folderTreeChildren; private String htmlDisplayName = null; - public ResultsNode() { + public ResultsNode(ResultModel model) { super(new FlatChildren()); this.flatChildren = (FlatChildren) this.getChildren(); - this.folderTreeChildren = new FolderTreeChildren(rootPathItem); + this.folderTreeChildren = + new FolderTreeChildren(rootPathItem); } void update() { @@ -387,6 +389,16 @@ */ private class FlatChildren extends Children.Keys { + public FlatChildren() { + resultModel.addPropertyChangeListener(ResultModel.PROP_REMOVED, + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + update(); + } + }); + } + @Override protected Node[] createNodes(MatchingObject key) { return new Node[]{createNodeForMatchingObject(key)}; @@ -412,8 +424,8 @@ } else { children = key.getDetailsChildren(replacing); } - MatchingObjectNode mon = - new MatchingObjectNode(delegate, children, key, replacing); + MatchingObjectNode mon = new MatchingObjectNode( + delegate, children, key, replacing); synchronized (this) { if (!closed) { matchingObjectNodes.add(mon); @@ -479,11 +491,11 @@ return; } } - parentItem.addChild(new FolderTreeItem(matchingObject)); + parentItem.addChild(new FolderTreeItem(matchingObject, parentItem)); } else { try { FolderTreeItem newChild = new FolderTreeItem( - DataObject.find(path.get(0))); + DataObject.find(path.get(0)), parentItem); parentItem.addChild(newChild); createInTreeView(newChild, path.subList(1, path.size()), matchingObject); @@ -497,20 +509,25 @@ private static final String PROP_SELECTED = "selected"; //NOI18N private static final String PROP_CHILDREN = "children"; //NOI18N + private FolderTreeItem parent; private DataObject folder = null; private MatchingObject matchingObject = null; private List children = new LinkedList(); private boolean selected = true; + private boolean removed = false; PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); /** * Constructor for root node */ public FolderTreeItem() { + this.parent = null; } - public FolderTreeItem(MatchingObject matchingObject) { + public FolderTreeItem(MatchingObject matchingObject, + FolderTreeItem parent) { + this.parent = parent; this.matchingObject = matchingObject; matchingObject.addPropertyChangeListener( new PropertyChangeListener() { @@ -520,16 +537,19 @@ if (pn.equals(MatchingObject.PROP_SELECTED)) { setSelected(FolderTreeItem.this.matchingObject .isSelected()); + } else if (pn.equals(MatchingObject.PROP_REMOVED)) { + remove(); } } }); } - public FolderTreeItem(DataObject file) { + public FolderTreeItem(DataObject file, FolderTreeItem parent) { + this.parent = parent; this.folder = file; } - void addChild(FolderTreeItem pathItem) { + synchronized void addChild(FolderTreeItem pathItem) { children.add(pathItem); firePropertyChange(PROP_CHILDREN, null, null); } @@ -538,8 +558,36 @@ return folder; } - public List getChildren() { - return children; + public synchronized List getChildren() { + return new ArrayList(children); + } + + public synchronized void remove() { + removed = true; + for (FolderTreeItem fti : children) { + if (fti.isPathLeaf()) { + fti.getMatchingObject().remove(); + } else { + fti.remove(); + } + } + if (parent != null) { + parent.removeChild(this); + } + } + + private synchronized boolean removeChild(FolderTreeItem child) { + if (!removed) { // needed to prevent ConcurrentModificationException + boolean result = children.remove(child); + if (children.isEmpty() && parent != null) { + remove(); + } else { + firePropertyChange(PROP_CHILDREN, null, null); + } + return result; + } else { + return false; + } } public MatchingObject getMatchingObject() { @@ -607,7 +655,13 @@ @Override public void propertyChange(PropertyChangeEvent evt) { fireIconChange(); - toggleParentSelected(FolderTreeNode.this); + String prop = evt.getPropertyName(); + if (prop.equals(FolderTreeItem.PROP_SELECTED)) { + toggleParentSelected( + FolderTreeNode.this.getParentNode()); + } else if (prop.equals(FolderTreeItem.PROP_CHILDREN)) { + toggleParentSelected(FolderTreeNode.this); + } } }); if (!pathItem.isPathLeaf()) { @@ -627,13 +681,25 @@ } @Override + public boolean canDestroy () { + return true; + } + + @Override + public void destroy () throws IOException { + FolderTreeItem folder = + this.getLookup().lookup(FolderTreeItem.class); + folder.remove(); + } + + @Override public Transferable drag() throws IOException { return UiUtils.DISABLE_TRANSFER; } @Override public Action[] getActions(boolean context) { - return new Action[0]; + return new Action[]{SystemAction.get(HideResultAction.class)}; } } @@ -675,8 +741,7 @@ } } - public static void toggleParentSelected(Node node) { - Node parent = node.getParentNode(); + public static void toggleParentSelected(Node parent) { if (parent == null) { return; } --- a/api.search/test/unit/src/org/netbeans/modules/search/TextDetailTest.java +++ a/api.search/test/unit/src/org/netbeans/modules/search/TextDetailTest.java @@ -144,7 +144,7 @@ public String createHtmlDisplayName(String line, String match) { TextDetail td = createMockTextDetail(line, match); - DetailNode detailNode = new TextDetail.DetailNode(td, false); + DetailNode detailNode = new TextDetail.DetailNode(td, false, null); String htmlDisplayName = detailNode.getHtmlDisplayName(); Pattern p = Pattern.compile("(\\s*\\d+: )(.*?)(\\s+\\[.*\\])"); Matcher m = p.matcher(htmlDisplayName);