Bye SaveCookie, Hello org.netbeans.api.actions.Savable

If you're using the NetBeans Platform as the basis of your own applications, you are going to find this particular blog entry, applicable to NetBeans Platform 7.1, extremely interesting!

When you're working with a modular framework such as the NetBeans Platform, your work consists of plugging your business logic into existing hooks provided by the framework. For example, rather than create your own Save button, you're going to want to reuse the existing Save button in the framework, assuming it has one. That way, the application won't have 100's of Save buttons; instead, there'll be a single Save button that will save whatever is currently in need of being saved.

That's where the NetBeans Platform's SaveCookie came into play. Implement the SaveCookie class, which means you need to create an implementation of the SaveCookie's "save()" method, put the implementation into the Lookup whenever there's something needing to be saved, and the Save button will automatically become enabled, delegating to your implementation, since the Save button (in fact, the SaveAction that's invokable from a button, menu item, and keyboard shortcut) is listening to the Lookup for the presence of SaveCookie implementations.

However, there have always been a number of problems with SaveCookie. Firstly, it is part of the Nodes API, meaning that you need to depend on that entire API even though you're only using the SaveCookie and possibly don't need to use any Nodes at all. Secondly, the SaveCookie is connected to DataObjects since, when you close the application, all unsaved DataObjects, i.e., all DataObjects with a SaveCookie in their Lookup, are displayed in a small Exit dialog, prompting you to save them prior to closing the application. However, what if your SaveCookie doesn't apply to a DataObject at all? Then you're out of luck since the Exit dialog will not display the fact that you have an outstanding SaveCookie. Thirdly, the "Save All" button (i.e., its underlying Action) would only become enabled for DataObjects, causing a lot of confusion when you're first working with SaveCookies. Finally, there was no way to modify the Exit dialog. E.g., maybe you'd like to change the text or add an icon. The only thing shown is the DataObject's display name, while you might have two DataObject's representing two versions of the same file open, one from the trunk and another from the branch. Wouldn't it be handy to show more than the display name, e.g., the path, to distinguish the two versions?

In 7.1, all the above problems are things of the past. Though, of course, since backward compatibility is holy in the NetBeans Team, you can continue using SaveCookies exactly as before. However, if you have a problem with any of the above limitations, you can switch to the org.netbeans.api.actions.Savable interface instead. There's even a default implementation, org.netbeans.spi.actions.AbstractSavable. Both of these are in the UI Utilities API, i.e., not in the Nodes API, and there's no connection with DataObjects at all. "Save All" now works automatically and the content of the Exit dialog (which now includes any objects with outstanding saves) is customizable.

public final class DemoTopComponent extends TopComponent implements DocumentListener {

    InstanceContent ic = new InstanceContent();

    public DemoTopComponent() {
        initComponents();
        setName(Bundle.CTL_DemoTopComponent());
        setToolTipText(Bundle.HINT_DemoTopComponent());
        associateLookup(new AbstractLookup(ic));
        jTextArea1.getDocument().addDocumentListener(this);
    }
 
    @Override
    public void insertUpdate(DocumentEvent e) {
        modify();
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        modify();
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
        modify();
    }

    private void modify() {
        if (getLookup().lookup(MySavable.class) == null) {
            ic.add(new MySavable());
        }
    }
 
    private static final Icon ICON = ImageUtilities.loadImageIcon("org/savable/Icon.png", true);
 
    private class MySavable extends AbstractSavable implements Icon {
        MySavable() {
            register();
        }
 
        @Override
        protected String findDisplayName() {
            try {
                final Document doc = jTextArea1.getDocument();
                String s = doc.getText(0, doc.getLength());
                int indx = s.indexOf('\n');
                if (indx >= 0) {
                    s = s.substring(0, indx);
                }
                return "First line '" + s + "'";
            } catch (BadLocationException ex) {
                return ex.getLocalizedMessage();
            }
        }

        @Override
        protected void handleSave() throws IOException {
            tc().ic.remove(this);
            unregister();
        }
 
        DemoTopComponent tc() {
            return DemoTopComponent.this;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof MySavable) {
                MySavable m = (MySavable)obj;
                return tc() == m.tc();
            }
            return false;
        }

        @Override
        public int hashCode() {
            return tc().hashCode();
        }

        @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();
        }
 
    } 

...     ...     ...

The code that you see above comes from the issue (see link to it at the end of this blog entry) where the need for all this functionality is outlined, solved, and integrated. It can be seen as the "hello world" sample of the new Savable interface.

Above, use is made of the new global registry for Savables. You register/unregister Savables into this registry, while the AbstractSavable provides the "register" and "unregister" calls so that you can let it do this work for you. If you don't register your Savable, the Exit dialog will not show information about the object's unsaved state (if the object is unsaved), when you close the application:


This also means some of the tutorials using SaveCookie need to be updated to use the new AbstractSavable class. For example, the NetBeans Platform CRUD Tutorial will be updated soon.

Related reading:

Finally, if your Savable also implements SaveAsCapable (requiring dependencies on the Nodes API, File System API, and Datasystems API), the "Save As" Action will become enabled and the "Save As" dialog will open when the Action is invoked. You'll also be able to implement the "saveAs" method, which is part of the SaveAsCapable interface, for customizing the Action.

Comments:

According to the documentation of AbstractSavable the save() method calls handleSave() and unregister(), so you can remove this from your implementation of handleSave().

Posted by Holger Stenger on December 14, 2011 at 10:07 PM PST #

Cool. I wish this was implemented earlier. it would have saved me so much trouble 2 years ago.

Posted by guest on December 16, 2011 at 01:18 AM PST #

Simply awesome!

I changed my code to use this feature and i'm finally able to get rid of the 'fake' DataObject i had to create just to get the Exit Dialog to show up. You could however enhance your example by overriding TopComponent.canClose() method. Here's what i came up with (notice that i'm using Cancellable interface too to clear any edits the user might have done):

@Override
public boolean canClose() {
Savable savable = getLookup().lookup(Savable.class);
if (savable == null) {
return true;
}
String msg = "Form '" + getDisplayName() + "' has unsaved data. Save it?";
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(msg, NotifyDescriptor.YES_NO_CANCEL_OPTION);
Object result = DialogDisplayer.getDefault().notify(nd);
if (result == NotifyDescriptor.CANCEL_OPTION || result == NotifyDescriptor.CLOSED_OPTION) {
return false;
}
if (result == NotifyDescriptor.NO_OPTION) {
Cancellable cancellable = getLookup().lookup(Cancellable.class);
if (cancellable != null) {
cancellable.cancel();
}
return true;
}
try {
savable.save();
return true;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
return false;
}
}

The reason i'm pasting this code is to show that this method is only coupled with NetBeans stuff. Not a single class of my own, whatsoever.
Pretty cool!

Best regards

Posted by metator on January 12, 2012 at 04:23 AM PST #

I have been go through source and found out the "unregister();" seem unnecessary on the sample code above.
The code "save" method on the AbstractSavable.java shows:
for (Savable s : Savable.REGISTRY.lookup(t).allInstances()) {
if (s == this) {
handleSave();
unregister();
return;
}
}
"Save All" button call to save method, "unregister()" are always been called aftet handleSave()......

Posted by Aknine on April 21, 2012 at 05:05 PM PDT #

I found that if you want to call the handleSave() method yourself (prior to performing an action for example) without forcing the user to click on the SaveAllAction button, then the unregister() needs to be in the handleSave() method or the SaveAllAction button will remain enabled...

Posted by jauten on April 27, 2012 at 11:01 AM PDT #

Is there anyway to set the file filter and initial folder for the Save As dialog? I have several document types with different file extensions and I would like the file saved in the project folder.

Posted by guest on November 15, 2012 at 09:17 AM PST #

Hi Geertjan,
This Savable , Abstract Savable API's cannot be found in Netbeans 7.0

What can I replace it with ?

Posted by Nigel Thomas on November 22, 2012 at 11:44 PM PST #

Why can't you use NetBeans Platform 7.2? And, if you must use 7.0, which I can't imagine why you need to use it, research the SaveCookie class, which is the old way of doing what is described in this blog entry. But you already knew that because that's exactly what this blog entry tells you.

Posted by Geertjan on November 22, 2012 at 11:49 PM PST #

thanks Geertjan. But if I want check something before the saving,when user input incorrect, alert and cancel the saving,how to implements it ?

Posted by guest on November 24, 2012 at 10:56 AM PST #

Haye you worked through the NetBeans Platform CRUD Application tutorial?

http://platform.netbeans.org/tutorials/nbm-crud.html

Posted by Geertjan on November 25, 2012 at 08:03 AM PST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
23
24
25
26
27
28
29
30
   
       
Today