diff --git a/api.search/src/org/netbeans/modules/search/MatchingObject.java b/api.search/src/org/netbeans/modules/search/MatchingObject.java --- a/api.search/src/org/netbeans/modules/search/MatchingObject.java +++ b/api.search/src/org/netbeans/modules/search/MatchingObject.java @@ -45,6 +45,7 @@ package org.netbeans.modules.search; import java.awt.EventQueue; +import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.BufferedReader; @@ -89,6 +90,10 @@ public static final String PROP_INVALIDITY_STATUS = "invalidityStatus"; //NOI18N public static final String PROP_SELECTED = "selected"; //NOI18N + /** Fired when the matching object is removed (hidden) from results. */ + public static final String PROP_REMOVED = "removed"; //NOI18N + /** Fired when some child of this object is removed from results. */ + public static final String PROP_CHILD_REMOVED = "child_removed"; //NOI18N /** */ private static final Logger LOG = @@ -269,6 +274,7 @@ } dataObject = null; nodeDelegate = null; + changeSupport.firePropertyChange(PROP_REMOVED, null, null); } private void setInvalid(InvalidityStatus invalidityStatus) { @@ -535,14 +541,14 @@ List detailNodes = new ArrayList(textDetails.size()); for (TextDetail txtDetail : textDetails) { - detailNodes.add(new TextDetail.DetailNode(txtDetail, false)); + detailNodes.add(new TextDetail.DetailNode(txtDetail, false, this)); } return detailNodes.toArray(new Node[detailNodes.size()]); } public Children getDetailsChildren(boolean replacing) { - return new DetailsChildren(replacing); + return new DetailsChildren(replacing, resultModel); } /** @@ -990,6 +996,27 @@ } /** + * Remove text detail, update precomputed values, inform listeners. + * + * @return True if the detail was removed, false otherwise. + */ + public void removeDetail(TextDetail textDetail) { + boolean removed = textDetails.remove(textDetail); + if (removed) { + matchesCount = getDetailsCount(); + resultModel.removeDetailMatch(this, textDetail); + changeSupport.firePropertyChange(PROP_CHILD_REMOVED, null, null); + } + } + + /** + * Remove this matching object from its result model and inform listeners. + */ + public void remove() { + resultModel.remove(this); + } + + /** * Bridge between new API and legacy implementation, will be deleted. */ public static class Def { @@ -1033,14 +1060,27 @@ private boolean replacing; - public DetailsChildren(boolean replacing) { + public DetailsChildren(boolean replacing, ResultModel model) { this.replacing = replacing; setKeys(getTextDetails()); + + MatchingObject.this.addPropertyChangeListener(PROP_CHILD_REMOVED, + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + update(); + } + }); } @Override protected Node[] createNodes(TextDetail key) { - return new Node[]{new TextDetail.DetailNode(key, replacing)}; + return new Node[]{new TextDetail.DetailNode(key, replacing, + MatchingObject.this)}; + } + + public void update() { + setKeys(getTextDetails()); } } diff --git a/api.search/src/org/netbeans/modules/search/ResultModel.java b/api.search/src/org/netbeans/modules/search/ResultModel.java --- a/api.search/src/org/netbeans/modules/search/ResultModel.java +++ b/api.search/src/org/netbeans/modules/search/ResultModel.java @@ -64,11 +64,12 @@ * @author Marian Petras */ public final class ResultModel { - public static final String PROP_SELECTION = "selection"; //NOI18N public static final String PROP_VALID = "valid"; //NOI18N public static final String PROP_MATCHING_OBJECTS = "matchingObjects"; //NOI18N + /** Fired when results were modified by the user. */ + public static final String PROP_RESULTS_EDIT = "resultsEdit"; //NOI18N /** */ private long startTime; @@ -115,6 +116,48 @@ isFullText = (basicCriteria != null) && basicCriteria.isFullText(); startTime = -1; } + + /** + * Remove the {@link MatchingObject} from the model and inform the + * listeners. + * + * @param mo Matching object to remove. + */ + public synchronized boolean remove(MatchingObject mo) { + if (matchingObjects.remove(mo)) { + totalDetailsCount -= mo.getMatchesCount(); + int deselected = 0; + if (mo.getTextDetails() != null) { + for (TextDetail td : mo.getTextDetails()) { + deselected += td.isSelected() ? -1 : 0; + } + } + mo.cleanup(); + // inform model listeners, old object contains removed object + propertyChangeSupport.firePropertyChange(PROP_RESULTS_EDIT, + null, null); + if (deselected < 0) { + updateSelected(deselected); + } + return true; + } + return false; + } + + public synchronized void removeDetailMatch(MatchingObject mo, + TextDetail txtDetail) { + + if (txtDetail.isSelected()) { + updateSelected(-1); + } + totalDetailsCount--; + propertyChangeSupport.firePropertyChange(PROP_RESULTS_EDIT, + null, null); + // delete parent node if no children left + if (mo.textDetails.isEmpty()) { + remove(mo); + } + } /** */ diff --git a/api.search/src/org/netbeans/modules/search/TextDetail.java b/api.search/src/org/netbeans/modules/search/TextDetail.java --- a/api.search/src/org/netbeans/modules/search/TextDetail.java +++ b/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 MatchingObject mo; /** * 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, + MatchingObject mo) { super(Children.LEAF, Lookups.fixed(txtDetail, new ReplaceCheckableNode(txtDetail, replacing))); this.txtDetail = txtDetail; + this.mo = mo; 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,15 @@ protected void createPasteTypes(Transferable t, List s) { } + @Override + public boolean canDestroy() { + return true; + } + + @Override + public void destroy() throws IOException { + this.mo.removeDetail(txtDetail); + } } // End of DetailNode class. /** diff --git a/api.search/src/org/netbeans/modules/search/ui/AbstractSearchResultsPanel.java b/api.search/src/org/netbeans/modules/search/ui/AbstractSearchResultsPanel.java --- a/api.search/src/org/netbeans/modules/search/ui/AbstractSearchResultsPanel.java +++ b/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(); diff --git a/api.search/src/org/netbeans/modules/search/ui/BasicAbstractResultsPanel.java b/api.search/src/org/netbeans/modules/search/ui/BasicAbstractResultsPanel.java --- a/api.search/src/org/netbeans/modules/search/ui/BasicAbstractResultsPanel.java +++ b/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 = @@ -95,6 +97,7 @@ protected BasicComposition composition; protected final ResultsOutlineSupport resultsOutlineSupport; private NodeListener resultsNodeAdditionListener; + private volatile boolean finished = false; protected static final boolean isMacLaf = "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N protected static final Color macBackground = @@ -120,8 +123,19 @@ setRootDisplayName(NbBundle.getMessage(ResultView.class, "TEXT_SEARCHING___")); //NOI18N initAccessibility(); + this.resultModel.addPropertyChangeListener( + ResultModel.PROP_RESULTS_EDIT, this); } + @Override + public void propertyChange(PropertyChangeEvent evt) { + // update the root node after change in model + if (finished) { + setFinalRootNodeText(); + } else { + updateRootNodeText(); + } + } public void update() { if (details && btnExpand.isVisible() && !btnExpand.isEnabled()) { @@ -246,6 +260,7 @@ @Override public void searchFinished() { super.searchFinished(); + this.finished = true; if (details && resultModel.size() > 0 && showDetailsButton != null) { showDetailsButton.setEnabled(true); } @@ -409,4 +424,4 @@ public void closed() { resultsOutlineSupport.closed(); } -} \ No newline at end of file +} diff --git a/api.search/src/org/netbeans/modules/search/ui/HideResultAction.java b/api.search/src/org/netbeans/modules/search/ui/HideResultAction.java new file mode 100644 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java b/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java --- a/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java +++ b/api.search/src/org/netbeans/modules/search/ui/MatchingObjectNode.java @@ -103,8 +103,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 +130,7 @@ MatchingObject.PROP_INVALIDITY_STATUS, validityListener); selectionListener = new SelectionListener(); - matchingObject.addPropertyChangeListener(MatchingObject.PROP_SELECTED, - selectionListener); + matchingObject.addPropertyChangeListener(selectionListener); } @Override @@ -165,7 +164,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 +234,7 @@ @Override public boolean canDestroy() { - return false; + return true; } public void clean() { @@ -274,6 +274,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 +449,7 @@ fireIconChange(); ResultsOutlineSupport.toggleParentSelected( - MatchingObjectNode.this); + MatchingObjectNode.this.getParentNode()); } } } diff --git a/api.search/src/org/netbeans/modules/search/ui/ResultsOutlineSupport.java b/api.search/src/org/netbeans/modules/search/ui/ResultsOutlineSupport.java --- a/api.search/src/org/netbeans/modules/search/ui/ResultsOutlineSupport.java +++ b/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,7 +313,7 @@ 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); @@ -387,6 +388,16 @@ */ private class FlatChildren extends Children.Keys { + public FlatChildren() { + resultModel.addPropertyChangeListener(ResultModel.PROP_RESULTS_EDIT, + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + update(); + } + }); + } + @Override protected Node[] createNodes(MatchingObject key) { return new Node[]{createNodeForMatchingObject(key)}; @@ -479,11 +490,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,6 +508,7 @@ 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 = @@ -508,9 +520,12 @@ * 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 +535,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 +556,38 @@ return folder; } - public List getChildren() { - return children; + public synchronized List getChildren() { + return new ArrayList(children); + } + + public synchronized void remove() { + // remove children first, then... + // NOTE uses a copy of the children to prevent a ConcurrentModifcationException + for (FolderTreeItem fti : new ArrayList(children)) { + if (fti.isPathLeaf()) { + // remove the matching node + fti.getMatchingObject().remove(); + } else { + // remove all folder children - starts a recursion + fti.remove(); + } + } + // ... then try to remove itself + if (parent != null) { + parent.removeChild(this); + } + } + + private synchronized void removeChild(FolderTreeItem child) { + boolean result = children.remove(child); + if (result) { + child.parent = null; + } + if (children.isEmpty() && parent != null) { + remove(); + } else { + firePropertyChange(PROP_CHILDREN, null, null); + } } 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,24 @@ } @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 +740,7 @@ } } - public static void toggleParentSelected(Node node) { - Node parent = node.getParentNode(); + public static void toggleParentSelected(Node parent) { if (parent == null) { return; } diff --git a/api.search/test/unit/src/org/netbeans/modules/search/TextDetailTest.java b/api.search/test/unit/src/org/netbeans/modules/search/TextDetailTest.java --- a/api.search/test/unit/src/org/netbeans/modules/search/TextDetailTest.java +++ b/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);