之前,筆者在開發 NetBeans RCP 的過程中,發生了一個非常怪異的問題。在 NetBeans Platform 的文件中指出,SaveAction 只要在 Lookup 中放入一個 Node 及一個 SaveCookie 就能夠啟用。但是,筆者不管怎麼試,都試不出來。最後,筆者也忘了是做了什麼事才讓它 enable ,可能是把某個 Netbeans 的 Module 加入之後吧。
最近,筆者又被 assign 了另一個專案,也是要用 NetBeans Platform 來設計。好死不死,筆者又遇到了一模一樣的問題(SaveAction 無法被 enable)。這次,筆者就決定把它給記錄下來。
這篇文章算是一篇記錄性質的文章,我們將討論一下,如何讓 SaveAction(呈現出來是在 File/Save 的選單中)被啟用。同時,我們也會討論一下,如何使用最少的程式來啟用 SaveAction 及 SaveAllAction。這些問題都是在 dev@openide.netbeans.org 討論串中常出現的文章。如果,大家曾經看過,或已經知道 work around ,就可以略過啦。
1. 問題描述
NetBeans 的 SaveAction 可以說是一個奇怪的玩意。 NetBeans 的文件中指出,SaveAction 要被啟用的條件是 Lookup 中有只有一個 Node ,且這個 Node 的 Lookup 有一個 SaveCookie 在裡面,筆者還去看過 SaveAction 的 source code 也是這樣設計的。但是,不論我們如何設計,都還是無法研 SaveAction 被啟用。根據 http://openide.netbeans.org/servlets/ReadMsg?listName=dev&msgNo=19141 這篇文章的討論,同樣的問題不止出現在 SaveAction ,還出現在 PrintAction 。本文也將根據上述文章的內容為主,來設計一個測試這個問題及其 work around 的專案。
2. 建立測試用的專案
我們要建立一個名叫 EnableSaveAction 的 module suite 及一個名叫 EnableSave 的 module 專案,並將 EnableSaveAction 專案設定成 Standalone Application。
3. 建立 TopComponent
我們接著要建立一個名叫 SaveAction 的 TopComponent,並在它的設計畫面中放入兩個 JButton 及一個 JTextArea ,如下圖。其中,JButton 分別用來啟用 Save 及停用 Save ;JTextArea 是用來顯示一些訊息。

4. 測試問題
首先,我們要建立一個沒用的 Node 及一個會顯示儲存訊號的 SaveCookie ,程式碼如下:
JAVA:
-
private SaveCookie m_SaveCookie;
-
private Node m_DummyNode;
-
-
public SaveCookie getSaveCookie(){
-
if(m_SaveCookie==null){
-
m_SaveCookie=new SaveCookie(){
-
-
jTextArea1.setText(jTextArea1.getText() + "I am saved.\r\n");
-
m_DummyDO.setModified(false);
-
}
-
};
-
}
-
return m_SaveCookie;
-
}
-
-
public Node getNode(){
-
if(m_DummyNode==null){
-
m_DummyNode=new AbstractNode(Children.LEAF, m_Lookup);
-
m_DummyNode.setName("Dummy Node");
-
m_DummyNode.setShortDescription("Useless node");
-
}
-
return m_DummyNode;
-
}
接著就是依照我們常用的方式來建立一個 InstanceContent 及 Lookup 如下:
JAVA:
-
private Lookup m_Lookup;
-
private InstanceContent m_InstanceContent;
-
private SaveActionTopComponent() {
-
initComponents();
-
setName(NbBundle.getMessage(SaveActionTopComponent.class, "CTL_SaveActionTopComponent"));
-
setToolTipText(NbBundle.getMessage(SaveActionTopComponent.class, "HINT_SaveActionTopComponent"));
-
// setIcon(Utilities.loadImage(ICON_PATH, true));
-
m_InstanceContent=new InstanceContent();
-
m_Lookup=new AbstractLookup(m_InstanceContent);
-
this.associateLookup(m_Lookup);
-
m_InstanceContent.add(getNode());
-
}
這段程式碼是在 SaveActionTopComponent 的 constructor 中設計的,所以在程式開始執行的時候,就會將我們所建立出來的 Dummy Node 加入 Lookup 中。
接下來,我們在 Enable Save(jButton1)及 Disable Save(jButton2)的兩個 JButton 中加入以下的程式碼:
JAVA:
-
private void jButton2ActionPerformed
(java.
awt.
event.
ActionEvent evt
) {
-
m_InstanceContent.remove(getSaveCookie());
-
jTextArea1.setText(jTextArea1.getText() + "remove save cookie.\r\n");
-
}
-
-
private void jButton1ActionPerformed
(java.
awt.
event.
ActionEvent evt
) {
-
m_InstanceContent.add(getSaveCookie());
-
jTextArea1.setText(jTextArea1.getText() + "put save cookie.\r\n");
-
}
在這段程式碼中,我們分別將 SaveCookie 加入到 InstanceContent 或從 InstanceContent 移除。
程式執行後,我們可以按下 Enable Save 的按鈕,將會發現 File/Save 並沒有被啟用,畫面如下:

5. 加入 SaveAction 的 work around
根據 http://openide.netbeans.org/servlets/ReadMsg?listName=dev&msgNo=19141 的討論,我們可以發現,只須要將 SaveAction 加到 Tool Bar 之中,就可以解決 SaveAction 的問題。所以,我們打開 layer.xml 檔,並加入以下的 XML :
XML:
-
<folder name="Toolbars">
-
<folder name="File">
-
<file name="SaveAction.shadow">
-
<attr name="originalFile" stringvalue="Actions/System/org-openide-actions-SaveAction.instance"/>
-
</file>
-
<attr name="com-xmlgem-xmleditor-action-OpenNewAction.shadow/org-openide-actions-SaveAllAction.instance" boolvalue="true"/>
-
</folder>
-
</folder>
加入之後,我們再次執行,並按下 Enable Save 的按鈕,發現 SaveAction 被正常地啟用。同時按下它,也能有對應的反應。畫面如下:

6. 加入 SaveAllAction 的 work around
SaveAllAction 的行為向來都是很正常的,所以這裡,我們將討論有沒有較簡單的方式可以啟用它。
這裡,我們將在記憶體中建立一個 Dummy.txt 的 FileObject ,並利用這個 FileObject 來得到系統預設的 DataObject 。透過這個 DataObject ,我們就可以利用 setModified 來讓 SaveAllAction 啟用了。
在設計之前,我們必須將 Datasystems API 及 File System API 給引用進來,完成後的畫面如下:

我們要在 SaveActionTopComponent 的 constructor 加入以下的程式碼,並多宣告一個 DataObject 的變數:
JAVA:
-
private DataObject m_DummyDO;
-
private Lookup m_Lookup;
-
private InstanceContent m_InstanceContent;
-
private SaveActionTopComponent() {
-
initComponents();
-
setName(NbBundle.getMessage(SaveActionTopComponent.class, "CTL_SaveActionTopComponent"));
-
setToolTipText(NbBundle.getMessage(SaveActionTopComponent.class, "HINT_SaveActionTopComponent"));
-
// setIcon(Utilities.loadImage(ICON_PATH, true));
-
m_InstanceContent=new InstanceContent();
-
m_Lookup=new AbstractLookup(m_InstanceContent);
-
this.associateLookup(m_Lookup);
-
m_InstanceContent.add(getNode());
-
-
//for enable SaveAll
-
FileObject oDummyFileObj=null;
-
try {
-
oDummyFileObj = FileUtil.createMemoryFileSystem().getRoot().createData("Dummy.txt");
-
-
ex.printStackTrace();
-
}
-
if(oDummyFileObj==null){
-
-
}
-
try {
-
-
m_DummyDO=DataObject.find(oDummyFileObj);
-
} catch (DataObjectNotFoundException ex) {
-
ex.printStackTrace();
-
}
-
}
接下來,我們修改一下 SaveCookie 及 Enable/Disable Save 的事件處理程式:
JAVA:
-
private void jButton2ActionPerformed
(java.
awt.
event.
ActionEvent evt
) {
-
m_InstanceContent.remove(getSaveCookie());
-
m_DummyDO.setModified(false);
-
jTextArea1.setText(jTextArea1.getText() + "remove save cookie.\r\n");
-
}
-
-
private void jButton1ActionPerformed
(java.
awt.
event.
ActionEvent evt
) {
-
m_InstanceContent.add(getSaveCookie());
-
m_DummyDO.setModified(true);
-
jTextArea1.setText(jTextArea1.getText() + "put save cookie.\r\n");
-
}
完成之後,可以再執行一次,並按下 Enable Save 的按鈕,我們將可以發現 Save 及 Save All 都被啟用了:

7. 討論
在這裡,我們可以使用這兩個 work around 來啟用 SaveAction 及 SaveAllAction 。但是,當我們按下 Enable Save 後,我們如果將程式關閉,NetBeans 則會顯示有個名叫 Dummy.txt 的檔案尚未被儲存,如下圖:

此時,Dummy.txt 就是我們之前在記憶體中建立的空白檔案,這邊,如果大家有須要的話,可以針對不同的情況建立不同名稱的檔案。
另外,如果我們按下 Save 或 Save All ,我們的 SaveCookie 將不會被呼叫到。這是因為,NetBeans 會透過 DataObject 來取得 SaveCookie ,然而,我們的 SaveCookie 是存在 InstanceContent 之中,並沒有存在 DataObject 中,所以不會被呼叫到。針對這個問題,我們可以使用 DataObject 的 Modified Property Change 事件來處理。
最後,筆者必須提一下,關於 SaveAction 的部份,筆者相信在不久的將來 Netbeans 的開發團隊會修正它,所以,大家如果採用這種方式,記得在程式碼中加個註記。關於 SaveAllAction 的部份,這裡所介紹的方法是在最少程式碼的方式來完成它,所以,有許多小細節會無法設定,例如 DataObject 的 icon …等。
完整的程式碼可以在這裡下載。