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 #

Hi, thanks for your post. I have a problem with Savable though.

When files are being saved locally I need to deploy them to a server. So I need to handle the save event. I did this implementing Savable:

public class VFDataObject extends MultiDataObject implements Savable {
.......
@Override
public void save() throws IOException {
.......
}
}

And it worked perfectly for the Save event. But then I realized I need to extend HtmlDataObject instead of MultiDataObject:

public class VFDataObject extends HtmlDataObject implements Savable {
.......
@Override
public void save() throws IOException {
.......
}
}

And now the save() doesn't get executed. Why? Since HtmlDataObject extends MultiDataObject. What should be done to make that work?

Thanks a lot.

Posted by Volodymyr on July 08, 2014 at 02:13 AM PDT #

I don't think HtmlDataObject is a public API class, is it? I would advise to avoid it.

Posted by Geertjan on July 08, 2014 at 02:18 AM PDT #

Hi, Geertjan, thanks for your great works.
i'm following this tutorial, save dialog only appear when application is closed but when i close the topComponent one by one save dialog is not showing.
The dialog showing later when application is about to close though.
How to make the save dialog appear the moment single topComponent closed, like netbeans way?
override componentClosed()method seems working but not clean and feel cumbersome for me.

Posted by wayan on July 29, 2014 at 05:21 AM PDT #

I have just a small query.
In the save Prompt, that appears on exit, a Help button is available.How can I implement that Help button.Currently on clicking that button ,nothing happens .But I want it to take me to the help Contents in my IDe

Posted by Sreedevi on September 23, 2014 at 08:09 AM PDT #

Hey Geertjan,

Thank you for a nice tutorial. I just have a question regarding the "Save All" capability and Savable.REGISTRY.

When a user clicks "Save All" button, I present a confirmation dialog to him, as in https://platform.netbeans.org/tutorials/nbm-crud.html. If he cancels/denies the confirmation dialog, is there a way to leave the AbstractSavable in the Savable.REGISTRY, as the user does not want to save that particular file at the moment, but might be interested in saving it later? I see that unregister() is always called after handleSave() in AbstractSavable, which is causing this behaviour.

I managed to overcome this by having static maxUID and non-static UID in static MySavable so that I can add new MySavable on cancel (if I just compare TopComponents in equals method, they are the same and add/register is ignored). But I think this is a bad coding practice, as MySavable should conceptually be an inner class, existing only in the scope of TopComponent and not a static inner class.

Additionally findDisplayName() + " saved." is shown in the bottom left corner of the IDE no matter what the user chooses after Save / Save All.

Thanks :)

Posted by Boris Perović on January 28, 2015 at 06:19 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
« May 2015
SunMonTueWedThuFriSat
     
2
3
8
9
10
19
24
25
26
27
28
29
30
31
      
Today