Commits

Jaroslav Tulach  committed cfcce18 Merge

Merge of savable branch with most recent default (after 7.0.1 was branched)

  • Participants
  • Parent commits e3e4aae, 4b97104
  • Branches savable-77210

Comments (0)

Files changed (24)

File core.ui/nbproject/project.xml

                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>7.22</specification-version>
+                        <specification-version>7.33</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>

File core.ui/src/org/netbeans/core/ui/resources/layer.xml

             <file name="org-openide-actions-FileSystemRefreshAction.instance"/>
             <file name="org-openide-actions-RenameAction.instance"/>
             <file name="org-openide-actions-ReorderAction.instance"/>
-            <file name="org-openide-actions-SaveAllAction.instance"/>
+            <file name="org-openide-actions-SaveAllAction.instance">
+                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
+                <attr name="context" methodvalue="org.netbeans.modules.openide.awt.SavableRegistry.getRegistry"/>
+                <attr name="delegate" newvalue="org.openide.actions.SaveAction"/>
+                <attr name="selectionType" stringvalue="ANY"/>
+                <attr name="surviveFocusChange" boolvalue="false"/>
+                <attr name="displayName" bundlevalue="org/openide/loaders/Bundle#SaveAll"/>
+                <attr name="noIconInMenu" boolvalue="true"/>
+                <attr name="iconBase" stringvalue="org/openide/loaders/saveAll.gif"/>
+                <attr name="type" stringvalue="org.netbeans.api.actions.Savable"/>
+            </file>
             <file name="org-openide-actions-SaveAction.instance">
                 <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
                 <attr name="delegate" newvalue="org.openide.actions.SaveAction"/>
                 <attr name="displayName" bundlevalue="org/openide/actions/Bundle#Save"/>
                 <attr name="noIconInMenu" boolvalue="true"/>
                 <attr name="iconBase" stringvalue="org/openide/resources/actions/save.png"/>
-                <attr name="type" stringvalue="org.openide.cookies.SaveCookie"/>
+                <attr name="type" stringvalue="org.netbeans.api.actions.Savable"/>
             </file>
             <file name="org-openide-actions-SaveAsAction.instance">
                 <attr name="instanceCreate" methodvalue="org.openide.actions.SaveAsAction.create"/>

File o.n.core/nbproject/project.xml

                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>7.17</specification-version>
+                        <specification-version>7.33</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>

File o.n.core/src/org/netbeans/core/ExitDialog.java

 
 import java.awt.Dimension;
 import java.beans.BeanInfo;
+import java.util.Collection;
 import java.util.ResourceBundle;
 import java.util.Set;
 import javax.swing.BorderFactory;
 import javax.swing.UIManager;
 import javax.swing.border.Border;
 import javax.swing.border.LineBorder;
+import org.netbeans.api.actions.Savable;
 import org.openide.DialogDescriptor;
 import org.openide.NotifyDescriptor;
 import org.openide.awt.Mnemonics;
-import org.openide.cookies.SaveCookie;
-import org.openide.loaders.DataObject;
 import org.openide.nodes.Node;
 import org.openide.util.Exceptions;
 import org.openide.util.ImageUtilities;
         setLayout (new java.awt.BorderLayout ());
 
         listModel = new DefaultListModel();
-        for (DataObject obj : DataObject.getRegistry().getModifiedSet()) {
+        for (Savable obj : Savable.REGISTRY.lookupAll(Savable.class)) {
             listModel.addElement(obj);
         }
         draw ();
         int index = 0;	// index of last removed item
 
         for (i = 0; i < count; i++) {
-            DataObject nextObject = (DataObject)array[i];
+            Savable nextObject = (Savable)array[i];
             index = listModel.indexOf(nextObject);
             save(nextObject);
         }
     /** Tries to save given data object using its save cookie.
      * Notifies user if excetions appear.
      */
-    private void save (DataObject dataObject) {
+    private void save (Savable sc) {
         try {
-            SaveCookie sc = dataObject.getLookup().lookup(SaveCookie.class);
             if (sc != null) {
                 sc.save();
             }
             // only remove the object if the save succeeded
-            listModel.removeElement(dataObject);
+            listModel.removeElement(sc);
         } catch (java.io.IOException exc) {
             Throwable t = exc;
             if (Exceptions.findLocalizedMessage(exc) == null) {
         // XXX(-ttran) result must be set before calling setVisible(false)
         // because this will unblock the thread which called Dialog.show()
         
-        for (int i = listModel.size() - 1; i >= 0; i--) {            
+/*        for (int i = listModel.size() - 1; i >= 0; i--) {            
             DataObject obj = (DataObject) listModel.getElementAt(i);
             obj.setModified(false);
         }
-
+*/
         result = true;
         exitDialog.setVisible (false);
         exitDialog.dispose();
      * Opens the ExitDialog.
      */
     private static boolean innerShowDialog() {
-        Set<DataObject> set = DataObject.getRegistry().getModifiedSet();
+        Collection<? extends Savable> set = Savable.REGISTRY.lookupAll(Savable.class);
         if (!set.isEmpty()) {
 
             // XXX(-ttran) caching this dialog is fatal.  If the user
                 boolean isSelected,      // is the cell selected
                 boolean cellHasFocus)    // the list and the cell have the focus
         {
-            final DataObject obj = (DataObject)value;
-            if (!obj.isValid()) {
-                // #17059: it might be invalid already.
-                // #18886: but if so, remove it later, otherwise BasicListUI gets confused.
-                SwingUtilities.invokeLater(new Runnable() {
-                    public void run() {
-                        listModel.removeElement(obj);
-                    }
-                });
-                setText("");
-                return this;
+            final Savable obj = (Savable)value;
+
+            if (obj instanceof Icon) {
+                super.setIcon((Icon)obj);
             }
-
-            Node node = obj.getNodeDelegate();
-
-            Icon icon = ImageUtilities.image2Icon(node.getIcon(BeanInfo.ICON_COLOR_16x16));
-            super.setIcon(icon);
-
-            setText(node.getDisplayName());
+            
+            setText(obj.toString());
             if (isSelected){
                 this.setBackground(UIManager.getColor("List.selectionBackground")); // NOI18N
                 this.setForeground(UIManager.getColor("List.selectionForeground")); // NOI18N

File openide.actions/src/org/openide/actions/SaveAction.java

 import java.util.logging.Logger;
 import javax.swing.AbstractAction;
 import javax.swing.Action;
+import org.netbeans.api.actions.Savable;
 import org.openide.DialogDisplayer;
 import org.openide.NotifyDescriptor;
 import org.openide.awt.StatusDisplayer;
 
 /** Save a one or more objects. Since version 6.20 this handles ANY selection
  * instead of EXACTLY_ONE selection.
+ * @see savable
  * @see SaveCookie
  *
  * @author   Jan Jancura, Petr Hamernik, Ian Formanek, Dafe Simonek
     }
 
     protected Class[] cookieClasses() {
-        return new Class[] { SaveCookie.class };
+        return new Class[] { Savable.class };
     }
 
     @Override
     }
 
     final void performAction(Lookup context) {
-        Collection<? extends SaveCookie> cookieList = context.lookupAll(SaveCookie.class);
+        Collection<? extends Savable> cookieList = context.lookupAll(Savable.class);
         Collection<? extends Node> nodeList = new LinkedList<Node>(context.lookupAll(Node.class));
 
-        COOKIE: for (SaveCookie saveCookie : cookieList) {
+        COOKIE: for (Savable savable : cookieList) {
 
-            //Determine if the saveCookie belongs to a node in our context
+            //Determine if the savable belongs to a node in our context
             for (Node node : nodeList) {
-                if (saveCookie.equals(node.getCookie(SaveCookie.class))) {
-                    performAction(saveCookie, node);
+                if (savable.equals(node.getLookup().lookup(Savable.class))) {
+                    performAction(savable, node);
                     nodeList.remove(node);
                     continue COOKIE;
                 }
             }
 
-            //The saveCookie was not found in any node in our context - save it by itself.
-            performAction(saveCookie, null);
+            //The savable was not found in any node in our context - save it by itself.
+            performAction(savable, null);
         }
     }
 
     protected void performAction(final Node[] activatedNodes) {
         for (int i = 0; i < activatedNodes.length; i++) {
             Node node = activatedNodes[i];
-            SaveCookie sc = node.getCookie(SaveCookie.class);
-            assert sc != null : "SaveCookie must be present on " + node + ". "
+            Savable sc = node.getLookup().lookup(Savable.class);
+            assert sc != null : "Savable must be present on " + node + ". "
                     + "See http://www.netbeans.org/issues/show_bug.cgi?id=68285 for details on overriding " + node.getClass().getName() + ".getCookie correctly.";
 
             // avoid NPE if disabled assertions
         }
     }
 
-    private void performAction(SaveCookie sc, Node n) {
+    private void performAction(Savable sc, Node n) {
         UserQuestionException userEx = null;
         for (;;) {
             try {
         return false;
     }
 
-    private String getSaveMessage(SaveCookie sc, Node n) {
+    private String getSaveMessage(Savable sc, Node n) {
         if (n == null) {
             return sc.toString();
         }
 
         @Override
         public boolean isEnabled() {
-            return context.lookup(SaveCookie.class) != null;
+            return context.lookup(Savable.class) != null;
         }
 
         public void actionPerformed(ActionEvent e) {

File openide.awt/apichanges.xml

 <apidef name="awt">AWT API</apidef>
 </apidefs>
 <changes>
+    <change id="Savable">
+        <api name="awt"/>
+        <summary>Savable context interface</summary>
+        <version major="7" minor="31"/>
+        <date day="27" month="3" year="2011"/>
+        <author login="jtulach"/>
+        <compatibility addition="yes" binary="compatible" deletion="no" semantic="compatible"/>
+        <description>
+            <p>
+                New interface for objects that can be saved. Old
+                <a href="@org-openide-nodes@/org/openide/cookies/SaveCookie.html">SaveCookie</a>
+                is retrofitted to implement the new <code>Savable</code> interface.
+            </p>
+        </description>
+        <class package="org.netbeans.api.actions" name="Savable"/>
+        <class package="org.netbeans.spi.actions" name="AbstractSavable"/>
+        <issue number="77210"/>
+    </change>
+    <change id="Actions.context-context">
+        <api name="awt"/>
+        <summary>Context Actions Can Specify Different Default Context</summary>
+        <version major="7" minor="31"/>
+        <date day="27" month="3" year="2011"/>
+        <author login="jtulach"/>
+        <compatibility addition="yes" binary="compatible" deletion="no" semantic="compatible"/>
+        <description>
+            <p>
+                <a href="@org-openide-awt@/org/openide/awt/Actions.html#context(java.lang.Class,%20boolean,%20boolean,%20org.openide.util.ContextAwareAction,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20boolean)">
+                Action.context</a>, when used from layer accepts <code>"context"</code>
+                attribute.
+            </p>
+        </description>
+        <class package="org.openide.awt" name="Actions"/>
+        <issue number="77210"/>
+    </change>
     <change id="ActionReference">
         <api name="awt"/>
         <summary>New @ActionReference annotations</summary>

File openide.awt/manifest.mf

 OpenIDE-Module: org.openide.awt
 OpenIDE-Module-Localizing-Bundle: org/openide/awt/Bundle.properties
 AutoUpdate-Essential-Module: true
-OpenIDE-Module-Specification-Version: 7.32
+OpenIDE-Module-Specification-Version: 7.33
 

File openide.awt/nbproject/project.xml

             </test-dependencies>
             <public-packages>
                 <package>org.netbeans.api.actions</package>
+                <package>org.netbeans.spi.actions</package>
                 <package>org.openide.awt</package>
             </public-packages>
         </data>

File openide.awt/src/org/netbeans/api/actions/Savable.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2010 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 2010 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.api.actions;
+
+import java.io.IOException;
+import org.netbeans.modules.openide.awt.SavableRegistry;
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.util.Lookup;
+
+/** Context interface that represents ability to persist its state to long term storage. To get best
+ * interaction with the system, it is preferable to use {@link AbstractSavable}
+ * to create instances of this interface rather than implementing it 
+ * directly.
+ *
+ * @author Jaroslav Tulach <jtulach@netbeans.org>
+ * @since 7.31
+ */
+public interface Savable {
+    /** Global registry of all {@link Savable}s that are modified in the
+     * application and subject to save by <em>Save All</em> action. See 
+     * {@link AbstractSavable} for description how to register your own
+     * implementation into the registry.
+     */
+    public static final Lookup REGISTRY = SavableRegistry.getRegistry();
+    
+    /** Invoke the save operation.
+     * @throws IOException if the object could not be saved
+     */
+    public void save() throws IOException;
+
+    /** Human descriptive, localized name of the savable. It is advised that
+     * all implementations of Savable override the toString method to provide
+     * human readable name.
+     * 
+     * @return human readable name representing the savable
+     */
+    @Override
+    public String toString();
+}

File openide.awt/src/org/netbeans/modules/openide/awt/SavableRegistry.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2011 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 2011 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.openide.awt;
+
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.util.Lookup;
+import org.openide.util.RequestProcessor;
+import org.openide.util.lookup.AbstractLookup;
+import org.openide.util.lookup.InstanceContent;
+
+/**
+ *
+ * @author Jaroslav Tulach <jtulach@netbeans.org>
+ */
+public final class SavableRegistry {
+    private static final RequestProcessor RP = new RequestProcessor("Savable Registry");
+    private static final InstanceContent IC = new InstanceContent(RP);
+    private static final Lookup LOOKUP = new AbstractLookup(IC);
+    
+    public static Lookup getRegistry() {
+        return LOOKUP;
+    }
+    
+    public static void register(AbstractSavable as) {
+        IC.add(as);
+    }
+    
+    public static void unregister(AbstractSavable as) {
+        IC.remove(as);
+    }
+}

File openide.awt/src/org/netbeans/spi/actions/AbstractSavable.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2010 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 2010 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.spi.actions;
+
+import java.io.IOException;
+import javax.swing.Icon;
+import org.netbeans.api.actions.Savable;
+import org.netbeans.modules.openide.awt.SavableRegistry;
+import org.openide.util.ImageUtilities;
+import org.openide.util.Lookup.Template;
+
+/** Default implementation of {@link Savable} interface and
+ * additional contracts, including dealing with {@link Savable#REGISTRY}.
+ * The human presentable name of the object to be saved is provided by
+ * implementing {@link #findDisplayName()}. In case this object wants 
+ * to be visually represented with an icon, it can also implement {@link Icon}
+ * interface (and delegate to {@link ImageUtilities#loadImageIcon(java.lang.String, boolean)}
+ * result). Here is example of typical implementation:
+ * <pre>
+class MySavable extends AbstractSavable {
+    private final Object obj;
+    public MySavable(Object obj) {
+        this.obj = obj;
+        register();
+    }
+    protected String findDisplayName() {
+        return "My name is " + obj.toString(); // get display name somehow
+    }
+    protected void handleSave() throws IOException {
+        // save 'obj' somehow
+    }
+    public boolean equals(Object other) {
+        if (other instanceof MySavable) {
+            return ((MySavable)other).obj.equals(obj);
+        }
+        return false;
+    }
+    public int hashCode() {
+        return obj.hashCode();
+    }
+}
+ * </pre>
+ *
+ * @author Jaroslav Tulach <jtulach@netbeans.org>
+ * @since 7.31
+ */
+public abstract class AbstractSavable implements Savable {
+    /** Constructor for subclasses. 
+     */
+    protected AbstractSavable() {
+    }
+
+    /** Implementation of {@link Savable#save} contract. Calls
+     * {@link #handleSave} and {@link #unregister}.
+     * 
+     * @throws IOException if call to {@link #handleSave} throws IOException
+     */
+    @Override
+    public final void save() throws IOException {
+        Template<AbstractSavable> t = new Template<AbstractSavable>(AbstractSavable.class, null, this);
+        for (Savable s : Savable.REGISTRY.lookup(t).allInstances()) {
+            if (s == this) {
+                handleSave();
+                unregister();
+                return;
+            }
+        }
+    }
+    
+    /** Finds suitable display name for the object this {@link Savable}
+     * represents.
+     * @return human readable, localized short string name
+     */
+    protected abstract String findDisplayName();
+    
+    /** To be overriden by subclasses to handle the actual save of 
+     * the object.
+     * 
+     * @throws IOException 
+     */
+    protected abstract void handleSave() throws IOException;
+
+    /** Equals and {@link #hashCode} need to be properly implemented 
+     * by subclasses to correctly implement equality contract. 
+     * Two {@link Savable}s should be equal
+     * if they represent the same underlying object beneath them. Without
+     * correct implementation proper behavior of {@link #register()} and
+     * {@link #unregister()} cannot be guaranteed.
+     * 
+     * @param obj object to compare this one to, 
+     * @return true or false
+     */
+    @Override
+    public abstract boolean equals(Object obj);
+
+    /** HashCode and {@link #equals} need to be properly implemented
+     * by subclasses, so two {@link Savable}s representing the same object
+     * beneath are really equal and have the same {@link #hashCode()}.
+     * @return integer hash
+     * @see #equals(java.lang.Object)
+     */
+    @Override
+    public abstract int hashCode();
+    
+    
+    /** Tells the system to register this {@link Savable} into {@link Savable#REGISTRY}.
+     * Only one {@link Savable} (according to {@link #equals(java.lang.Object)} and
+     * {@link #hashCode()}) can be in the registry. New call to {@link #register()}
+     * replaces any previously registered and equal {@link Savable}s. After this call
+     * the {@link Savable#REGISTRY} holds a strong reference to <code>this</code>
+     * which prevents <code>this</code> object to be garbage collected until it
+     * is {@link #unregister() unregistered} or {@link #register() replaced by
+     * equal one}.
+     */
+    protected final void register() {
+        SavableRegistry.register(this);
+    }
+    
+    /** Removes this {@link Savable} from the {@link Savable#REGISTRY} (if it 
+     * is present there, by relying on {@link #equals(java.lang.Object)} 
+     * and {@link #hashCode()}). 
+     */
+    protected final void unregister() {
+        SavableRegistry.unregister(this);
+    }
+
+    @Override
+    public final String toString() {
+        return findDisplayName();
+    }
+}

File openide.awt/src/org/netbeans/spi/actions/package.html

+<!--
+   - DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+   -
+   - Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
+   -
+   - 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.  Sun designates this
+   - particular file as subject to the "Classpath" exception as provided
+   - by Sun 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]"
+   -
+   - Contributor(s):
+   -
+   - The Original Software is NetBeans. The Initial Developer of the Original
+   - Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
+   - Microsystems, Inc. All Rights Reserved.
+   -
+   - 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.
+  -->
+
+<html>
+<head><title>org.netbeans.spi.actions</title></head>
+<body>
+
+Contains support classes for easier implementation of <em>Context Interfaces</em>.
+</body>
+</html>

File openide.awt/src/org/openide/awt/Actions.java

      *   &lt;attr name="displayName" bundlevalue="your.pkg.Bundle#key"/&gt;
      *   &lt;attr name="iconBase" stringvalue="your/pkg/YourImage.png"/&gt;
      *   &lt;!-- if desired: &lt;attr name="noIconInMenu" boolvalue="true"/&gt; --&gt;
+     *   &lt;!-- since 7.31: &lt;attr name="context" newvalue="org.my.own.LookupImpl"/&gt; --&gt;
      * &lt;/file&gt;
      * </pre>
      * Now the constructor of <code>YourClass</code> needs to have following
         return context(map);
     }
     static ContextAwareAction context(Map fo) {
-        return GeneralAction.context(fo);
+        ContextAwareAction caa = GeneralAction.context(fo);
+        Object context = fo.get("context");
+        if (context instanceof Lookup) {
+            return (ContextAwareAction)caa.createContextAwareInstance((Lookup)context);
+        }
+        return caa;
     }
     static ContextAction.Performer<?> inject(final Map fo) {
         Object t = fo.get("selectionType"); // NOI18N

File openide.awt/test/unit/src/org/netbeans/api/actions/SavableTest.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2010 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 2010 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.api.actions;
+
+import java.io.IOException;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.util.Lookup.Result;
+import org.openide.util.LookupEvent;
+import org.openide.util.LookupListener;
+
+/**
+ *
+ * @author Jaroslav Tulach <jtulach@netbeans.org>
+ */
+public class SavableTest extends NbTestCase {
+
+    public SavableTest(String n) {
+        super(n);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        for (DoSave savable : Savable.REGISTRY.lookupAll(DoSave.class)) {
+            savable.cleanup();
+        }
+    }
+
+    public void testSavablesAreRegistered() throws IOException {
+        String id = "identity";
+        DoSave savable = new DoSave(id, null, null);
+        assertNotNull("Savable created", savable);
+        
+        assertTrue(
+            "Is is among the list of savables that need save", 
+            Savable.REGISTRY.lookupAll(Savable.class).contains(savable)
+        );
+        
+        savable.save();
+        assertTrue("called", savable.save);
+        
+        assertTrue("No other pending saves", Savable.REGISTRY.lookupAll(Savable.class).isEmpty());
+    }
+
+    public void testTwoSavablesForEqual() throws IOException {
+        Object id = new Object();
+        
+        DoSave s = new DoSave(id, null, null);
+        assertEquals("The first", s, Savable.REGISTRY.lookup(Savable.class));
+        DoSave s2 = new DoSave(id, null, null);
+        
+        assertEquals("Only one savable", 1, Savable.REGISTRY.lookupAll(Savable.class).size());
+        assertEquals("The later", s2, Savable.REGISTRY.lookup(Savable.class));
+        
+        s.save();
+        assertFalse("Calling save on replaced savables has no effect", s.save);
+    }
+
+    public void testEventDeliveredAsynchronously() throws Exception {
+        class L implements LookupListener {
+            int change;
+            Object id = new Object();
+            
+            @Override
+            public synchronized void resultChanged(LookupEvent ev) {
+                change++;
+                notifyAll();
+            }
+            
+            public synchronized void createSavable() {
+                assertEquals("No changes yet", 0, change);
+                Savable s = new DoSave(id, null, null);
+                assertEquals("The first", s, Savable.REGISTRY.lookup(Savable.class));
+                assertEquals("Still no changes", 0, change);
+            }
+            
+            public synchronized void waitForChange() throws InterruptedException {
+                while (change == 0) {
+                    wait();
+                }
+                assertEquals("One change delivered", 1, change);
+            }
+        }
+        L listener = new L();
+        Result<Savable> res = Savable.REGISTRY.lookupResult(Savable.class);
+        
+        try {
+            res.addLookupListener(listener);
+            listener.createSavable();
+            listener.waitForChange();
+        } finally {
+            res.removeLookupListener(listener);
+        }
+    }
+    
+    static class DoSave extends AbstractSavable {
+        boolean save;
+        private final Object id;
+        private final CharSequence displayName, ch2;
+
+        public DoSave(Object id, CharSequence displayName, CharSequence ch2) {
+            this.id = id;
+            this.displayName = displayName;
+            this.ch2 = ch2;
+            register();
+        }
+
+        @Override
+        public String findDisplayName() {
+            return displayName.toString();
+        }
+
+        @Override
+        protected void handleSave() throws IOException {
+            save = true;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof DoSave) {
+                return ((DoSave)obj).id.equals(id);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return id.hashCode();
+        }
+
+        final void cleanup() {
+            unregister();
+        }
+    }
+    
+}

File openide.awt/test/unit/src/org/openide/awt/ContextActionInjectTest.java

     }
 
     public void testContextAction() throws Exception {
+        Context.cnt = 0;
+        
         FileObject fo = FileUtil.getConfigFile(
             "actions/support/test/testInjectContext.instance"
         );
         clone.actionPerformed(new ActionEvent(this, 200, ""));
         assertEquals("Global Action stays same", 10, Context.cnt);
     }
+    
+    private static final InstanceContent contextI = new InstanceContent();
+    private static final Lookup context = new AbstractLookup(contextI);
+    public static Lookup context() {
+        return context;
+    }
+    
+    public void testOwnContextAction() throws Exception {
+        MultiContext.cnt = 0;
+        
+        FileObject fo = FileUtil.getConfigFile(
+            "actions/support/test/testOwnContext.instance"
+        );
+        assertNotNull("File found", fo);
+        Object obj = fo.getAttribute("instanceCreate");
+        assertNotNull("Attribute present", obj);
+        assertTrue("It is context aware action", obj instanceof ContextAwareAction);
+        ContextAwareAction a = (ContextAwareAction)obj;
+
+        InstanceContent ic = contextI;
+        ic.add(10);
+
+        assertEquals("Number lover!", a.getValue(Action.NAME));
+        a.actionPerformed(new ActionEvent(this, 300, ""));
+        assertEquals("Global Action not called", 10, MultiContext.cnt);
+
+        ic.remove(10);
+        a.actionPerformed(new ActionEvent(this, 200, ""));
+        assertEquals("Global Action stays same", 10, MultiContext.cnt);
+    }
 
     public static final class MultiContext implements ActionListener {
         private final List<Number> context;
     }
 
     public void testMultiContextAction() throws Exception {
+        MultiContext.cnt = 0;
+        
         FileObject fo = FileUtil.getConfigFile(
             "actions/support/test/testInjectContextMulti.instance"
         );

File openide.awt/test/unit/src/org/openide/awt/test-layer.xml

                     <attr name='key' stringvalue='contextKey'/>
                     <attr name="type" stringvalue="java.lang.Number"/>
                 </file>
+                <file name="testOwnContext.instance">
+                    <attr name='instanceCreate' methodvalue='org.openide.awt.Actions.context'/>
+                    <attr name='context' methodvalue='org.openide.awt.ContextActionInjectTest.context'/>
+                    <attr name='delegate' methodvalue='org.openide.awt.Actions.inject'/>
+                    <attr name='injectable' stringvalue='org.openide.awt.ContextActionInjectTest$MultiContext'/>
+                    <attr name="selectionType" stringvalue="ANY"/>
+                    <attr name='displayName' bundlevalue='org/openide/awt/TestBundle#NumberLover'/>
+                    <attr name='iconBase' methodvalue='org.openide.awt.ContextActionTest.myIconResource'/>
+                    <attr name='noIconInMenu' boolvalue="true"/>
+                    <attr name='key' stringvalue='contextKey'/>
+                    <attr name="type" stringvalue="java.lang.Number"/>
+                </file>
                 <file name="testInjectContextLookup.instance">
                     <attr name='instanceCreate' methodvalue='org.openide.awt.Actions.context'/>
                     <attr name='delegate' newvalue='org.openide.awt.ContextActionInjectTest$LookupContext'/>

File openide.loaders/nbproject/project.xml

                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>6.5</specification-version>
+                        <specification-version>7.33</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>

File openide.loaders/src/org/openide/loaders/DataObject.java

 package org.openide.loaders;
 
 
+import java.awt.Component;
+import java.awt.Graphics;
 import java.beans.*;
 import java.io.*;
 import java.text.DateFormat;
 import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javax.swing.Icon;
 import javax.swing.event.*;
+import org.netbeans.api.actions.Savable;
 import org.netbeans.modules.openide.loaders.DataObjectAccessor;
 import org.netbeans.modules.openide.loaders.DataObjectEncodingQueryImplementation;
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.cookies.SaveCookie;
 import org.openide.filesystems.*;
 import org.openide.nodes.*;
 import org.openide.util.*;
 
     /** all modified data objects contains DataObjects.
     * ! Use syncModified for modifications instead !*/
-    private static ModifiedRegistry modified = new ModifiedRegistry();
+    private static final ModifiedRegistry modified = new ModifiedRegistry();
     /** sync modified data (for modification operations) */
     private static final Set<DataObject> syncModified = Collections.synchronizedSet(modified);
 
     public void setModified(boolean modif) {
         if (this.modif != modif) {
             this.modif = modif;
+            Savable present = getLookup().lookup(AbstractSavable.class);
             if (modif) {
                 syncModified.add (this);
+                if (present == null) {
+                    new DOSavable(this).add();
+                }
             } else {
                 syncModified.remove (this);
+                if (present == null) {
+                    new DOSavable(this).remove();
+                }
             }
             firePropertyChange(DataObject.PROP_MODIFIED,
                                !modif ? Boolean.TRUE : Boolean.FALSE,
         */
         public Set<DataObject> getModifiedSet() {
             synchronized (syncModified) {
-                return new HashSet<DataObject>(syncModified);
+                HashSet<DataObject> set = new HashSet<DataObject>(syncModified);
+                return set;
             }
         }
 
         * @return array of objects
         */
         public DataObject[] getModified () {
-            return syncModified.toArray(new DataObject[0]);
+            return getModifiedSet().toArray(new DataObject[0]);
         }
     }
 
         }
 
     }  // end of ModifiedRegistry inner class
+    
+    private static final class DOSavable extends AbstractSavable 
+    implements Icon {
+        final DataObject obj;
+
+        public DOSavable(DataObject obj) {
+            this.obj = obj;
+        }
+
+        @Override
+        public String findDisplayName() {
+            return obj.getNodeDelegate().getDisplayName();
+        }
+
+        @Override
+        protected void handleSave() throws IOException {
+            SaveCookie sc = obj.getCookie(SaveCookie.class);
+            if (sc != null) {
+                sc.save();
+            }
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof DOSavable) {
+                DOSavable dos = (DOSavable)other;
+                return obj.equals(dos.obj);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return obj.hashCode();
+        }
+
+        final void remove() {
+            unregister();
+        }
+
+        final void add() {
+            register();
+        }
+
+        @Override
+        public void paintIcon(Component c, Graphics g, int x, int y) {
+            icon().paintIcon(c, g, x, y);
+        }
+
+        @Override
+        public int getIconWidth() {
+            return icon().getIconWidth();
+        }
+
+        @Override
+        public int getIconHeight() {
+            return icon().getIconHeight();
+        }
+        
+        private Icon icon() {
+            return ImageUtilities.image2Icon(obj.getNodeDelegate().getIcon(BeanInfo.ICON_COLOR_16x16));
+        }
+    }
 
     /** A.N. - profiling shows that MultiLoader.checkFiles() is called too often
     * This method is part of the fix - empty for DataObject.

File openide.loaders/test/unit/src/org/openide/loaders/DataGetModifiedTest.java

 
 package org.openide.loaders;
 
+import java.util.Collection;
 import java.util.Iterator;
+import javax.swing.Icon;
+import javax.swing.event.ChangeEvent;
 import org.openide.filesystems.*;
-import java.beans.*;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.actions.Savable;
 import org.netbeans.junit.*;
 import org.openide.cookies.EditorCookie;
+import org.openide.cookies.SaveCookie;
 
 public class DataGetModifiedTest extends NbTestCase {
 
     @Override
     protected void tearDown() throws Exception {
         TestUtilHid.destroyLocalFileSystem (getName());
+        for (Savable s : Savable.REGISTRY.lookupAll(Savable.class)) {
+            s.save();
+        }
+        Collection<? extends Savable> empty = Savable.REGISTRY.lookupAll(Savable.class);
+        assertTrue("registry is emptied: " + empty, empty.isEmpty());
     }
 
     
         assertEquals("But now visible", 3, DataObject.getRegistry().getModifiedSet().size());
     }
     
+    public void testSavableRegistry() throws Exception {
+        class L implements ChangeListener {
+            int cnt;
+
+            @Override
+            public void stateChanged(ChangeEvent e) {
+                assertTrue(e.getSource() instanceof Collection);
+                for (Object o : (Collection)e.getSource()) {
+                    assertTrue("DataObject is the value: " + o, o instanceof DataObject);
+                }
+                cnt++;
+            }
+            
+        }
+        L listener = new L();
+        
+        DataObject.getRegistry().addChangeListener(listener);
+        do1.getLookup().lookup(EditorCookie.class).openDocument().insertString(0, "Ahoj", null);
+        String name = do1.getNodeDelegate().getDisplayName();
+        assertTrue("DataObject is modified", do1.isModified());
+        assertEquals("One change in registry", 1, listener.cnt);
+
+        Savable savable = findSavable(name);
+        assertNotNull("Savable for the do1 lookup found", savable);
+        savable.save();
+        assertFalse("DataObject no longer modified", do1.isModified());
+        assertEquals("2nd change in registry", 2, listener.cnt);
+        
+        do1.getLookup().lookup(EditorCookie.class).openDocument().insertString(0, "Ahoj", null);
+        assertTrue("DataObject is modified again", do1.isModified());
+        assertEquals("3rd change in registry", 3, listener.cnt);
+        
+        Savable another = findSavable(name);
+        assertNotSame("It is different instance", savable, another);
+        assertEquals("But it remains equals", savable, another);
+        assertTrue("DataObject savables provide Icons", another instanceof Icon);
+        
+        savable.save();
+        assertTrue("Calling save on old savable has no impact", do1.isModified());
+        
+        SaveCookie sc = do1.getLookup().lookup(SaveCookie.class);
+        sc.save();
+        assertFalse("Unmodified", do1.isModified());
+        
+        Savable none = findSavable(name);
+        assertNull("No savable for our dataobject found", none);
+    }
+
+    private Savable findSavable(String name) {
+        Savable savable = null;
+        for (Savable s : Savable.REGISTRY.lookupAll(Savable.class)) {
+            if (s.toString().equals(name)) {
+                savable = s;
+                break;
+            }
+        }
+        return savable;
+    }
+    
     
     private String fsstruct [] = new String [] {
         "Dir/SubDir/X.txt",

File openide.loaders/test/unit/src/org/openide/loaders/SavableDataObjectTest.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2011 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 2011 Sun Microsystems, Inc.
+ */
+package org.openide.loaders;
+
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.util.Enumeration;
+import javax.swing.Action;
+import org.netbeans.api.actions.Savable;
+import org.netbeans.junit.MockServices;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.spi.actions.AbstractSavable;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.ContextAwareAction;
+import org.openide.util.Enumerations;
+
+/**
+ */
+public class SavableDataObjectTest extends NbTestCase {
+    public SavableDataObjectTest(String n) {
+        super(n);
+    }
+
+    @Override
+    protected boolean runInEQ() {
+        return true;
+    }
+    
+    @Override
+    protected void setUp() throws Exception {
+        clearWorkDir();
+        MockServices.setServices(Pool.class);
+    }
+    
+    public void testHowManySavables() throws Exception {
+        FileObject root = FileUtil.toFileObject(getWorkDir());
+        FileObject test = root.createData("test.save");
+        
+        DataObject obj = DataObject.find(test);
+        assertEquals("Right type", SavaObj.class, obj.getClass());
+        
+        Action a = findSaveAction().createContextAwareInstance(obj.getNodeDelegate().getLookup());
+        Action all = findSaveAllAction();
+        assertFalse("Disabled at first", a.isEnabled());
+        assertFalse("All Disabled at first", all.isEnabled());
+        
+        obj.setModified(true);
+        
+        assertTrue("Enabled", a.isEnabled());
+        assertTrue("All enabled too", all.isEnabled());
+        
+        assertEquals("One modified object", 1, Savable.REGISTRY.lookupAll(Savable.class).size());
+        assertTrue("Old registry contains it as well", DataObject.getRegistry().getModifiedSet().contains(obj));
+        
+        all.actionPerformed(new ActionEvent(this, 0, ""));
+        assertEquals("One save", 1, ((SavaObj)obj).handleSave);
+        
+        assertFalse("Disabled at end", a.isEnabled());
+        assertFalse("All Disabled at end", all.isEnabled());
+        
+    }
+    
+    
+    private static ContextAwareAction findSaveAction() {
+        FileObject fo = FileUtil.getConfigFile("Actions/System/org-openide-actions-SaveAction.instance");
+        assertNotNull("File found", fo);
+        Object obj = fo.getAttribute("instanceCreate");
+        assertTrue("action found: " + obj, obj instanceof ContextAwareAction);
+        return (ContextAwareAction)obj;
+    }
+    private static Action findSaveAllAction() {
+        FileObject fo = FileUtil.getConfigFile("Actions/System/org-openide-actions-SaveAllAction.instance");
+        assertNotNull("File found", fo);
+        Object obj = fo.getAttribute("instanceCreate");
+        assertTrue("action found: " + obj, obj instanceof ContextAwareAction);
+        return (Action)obj;
+    }
+    
+    public static final class Pool extends DataLoaderPool {
+        @Override
+        protected Enumeration<? extends DataLoader> loaders() {
+            return Enumerations.singleton(SavaLoader.getLoader(SavaLoader.class));
+        }
+    } // end of Pool
+    
+    public static final class SavaLoader extends UniFileLoader {
+        public SavaLoader() {
+            super(SavaObj.class.getName());
+        }
+        
+        @Override
+        protected void initialize() {
+            super.initialize();
+            getExtensions().addExtension("save");
+        }
+
+        @Override
+        protected MultiDataObject createMultiObject(FileObject primaryFile) throws DataObjectExistsException, IOException {
+            return new SavaObj(primaryFile, this);
+        }
+    }
+    
+    private static final class SavaObj extends MultiDataObject {
+        int handleSave;
+        
+        public SavaObj(FileObject pf, SavaLoader l) throws DataObjectExistsException {
+            super(pf, l);
+        }
+
+        @Override
+        public void setModified(boolean modif) {
+            if (modif) {
+                getCookieSet().assign(Savable.class, new SaveMe());
+            } else {
+                SaveMe prev = getCookieSet().getLookup().lookup(SaveMe.class);
+                getCookieSet().assign(Savable.class);
+                if (prev != null) {
+                    prev.discard();
+                }
+            }
+            super.setModified(modif);
+        }
+        
+        private class SaveMe extends AbstractSavable {
+            public SaveMe() {
+                register();
+            }
+            
+            @Override
+            public String findDisplayName() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            protected void handleSave() throws IOException {
+                handleSave++;
+                setModified(false);
+            }
+            
+            private DataObject obj() {
+                return SavaObj.this;
+            }
+
+            @Override
+            public boolean equals(Object obj) {
+                if (obj instanceof SaveMe) {
+                    SaveMe other = (SaveMe)obj;
+                    return obj().equals(other.obj());
+                }
+                return false;
+            }
+
+            @Override
+            public int hashCode() {
+                return obj().hashCode();
+            }
+
+            final void discard() {
+                unregister();
+            }
+        }
+    }
+}

File openide.nodes/manifest.mf

 OpenIDE-Module: org.openide.nodes
 OpenIDE-Module-Localizing-Bundle: org/openide/nodes/Bundle.properties
 AutoUpdate-Essential-Module: true
-OpenIDE-Module-Specification-Version: 7.22
+OpenIDE-Module-Specification-Version: 7.23
 

File openide.nodes/module-auto-deps.xml

+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 1997-2010 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]"
+
+Contributor(s):
+
+The Original Software is NetBeans. The Initial Developer of the Original
+Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
+Microsystems, Inc. All Rights Reserved.
+
+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.
+-->
+
+<!DOCTYPE transformations PUBLIC "-//NetBeans//DTD Module Automatic Dependencies 1.0//EN" "http://www.netbeans.org/dtds/module-auto-deps-1_0.dtd">
+
+<transformations version="1.0">
+    <transformationgroup>
+        <description>No need for separate templates API. Merged into org.openide.loaders</description>
+        <transformation>
+            <trigger-dependency type="older">
+                <module-dependency codenamebase="org.openide.nodes" spec="7.21"/>
+            </trigger-dependency>
+            <implies>
+                <result>
+                    <module-dependency codenamebase="org.openide.awt"/>
+                </result>
+            </implies>
+        </transformation>
+    </transformationgroup>
+
+</transformations>

File openide.nodes/nbproject/project.xml

                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>7.10</specification-version>
+                        <specification-version>7.31</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>

File openide.nodes/src/org/openide/cookies/SaveCookie.java

  */
 package org.openide.cookies;
 
+import org.netbeans.api.actions.Savable;
 import org.openide.nodes.Node;
 
 
-/** The cookie for the save operation.
+/** The cookie for the save operation. Since 7.21 it implements
+* {@link Savable}.
 *
 * @author Dafe Simonek
 */
-public interface SaveCookie extends Node.Cookie {
-    /** Invoke the save operation.
-     * @throws IOException if the object could not be saved
-     */
-    public void save() throws java.io.IOException;
+public interface SaveCookie extends Node.Cookie, Savable {
 }