Eclipse 中 Editor 內建完整的儲存機制(isDirty()、doSave() 等),但 View 主要用於樹狀導航,預設不支援「另存新檔」等操作。然而實際開發中,常需在導航視圖直接保存多個編輯器的修改。Common Navigator Framework (CNF) 提供了 Saveable Protocol 解決此需求。本文深入剖析其原理並附完整實作範例。
EditorPart 核心方法:
isDirty():回傳 true 時標題顯示 *,工具列「儲存」啟用doSave(IProgressMonitor):執行儲存邏輯,成功後將 dirty 旗標重設為 false 並觸發 firePropertyChange()isSaveAsAllowed() / doSaveAs():支援「另存新檔」ViewPart 則無此類方法,因設計定位為「導航」而非「編輯」。
關鍵介面與類別:
CommonNavigator 實作,提供可儲存物件清單ContentProvider 需適配此介面,負責樹節點與可儲存物件映射doSave()、isDirty() 等方法SaveablesProvider 關鍵方法:
public abstract Saveable[] getSaveables(); // 所有可儲存物件
public abstract Object[] getElements(Saveable s); // Saveable 對應的樹節點模型
public abstract Saveable getSaveable(Object element); // 樹節點對應的 Saveable
// 通知方法(必須在 UI 執行緒呼叫)
fireSaveablesOpened(Saveable[]);
fireSaveablesClosing(Saveable[], boolean);
fireSaveablesClosed(Saveable[]);
流程圖解:
firePropertyChange(PROP_DIRTY)SaveAction、SaveAllAction)透過 getActiveSaveables() 計算 dirty 狀態*Saveable.doSave() 執行持久化public class SaveableView extends CommonNavigator {
public static String ID = "ViewSaveableProtocol.SaveableView";
public Saveable[] getSaveables() {
return this.getActiveSaveables(); // 僅回傳活動狀態的 Saveable
}
public void fireSaveabelsChanged() {
this.firePropertyChange(IWorkbenchPartConstants.PROP_DIRTY);
}
}
在 org.eclipse.ui.navigator 擴展點指定 View 類別為 org.eclipse.ui.navigator.CommonNavigator。
在 org.eclipse.ui.navigator.navigatorContent 擴展點設定 providesSaveables="true"。ContentProvider 需繼承 SaveablesProvider 並實作 ITreeContentProvider、IAdaptable:
public class SaveableContentProvider extends SaveablesProvider
implements ITreeContentProvider, IAdaptable {
@Override
public Object[] getElements(Saveable saveable) {
if (saveable instanceof SaveablePart) {
IWorkbenchPart part = ((SaveablePart)saveable).getWorkbenchPart();
IEditorInput editorInput = ((TextFileEditor)part).getEditorInput();
TextFile file = ((TextFileEditorInput)editorInput).getTextFile();
return new Object[]{ file };
}
return null;
}
@Override
public Saveable getSaveable(Object element) {
if (element instanceof TextFile) {
IWorkbenchPart part = FolderManager.getWorkbenchPart((TextFile) element);
if (part != null) return new SaveablePart(part);
}
return null;
}
@Override
public Saveable[] getSaveables() {
Object[] parts = FolderManager.getAllOpenedWorkbenchPart();
return new Saveable[parts.length]; // 實際需填入 SaveablePart 陣列
}
}
使用 org.eclipse.ui.decorators 擴展點,實作 ILightweightLabelDecorator:
public class FileModifiedDecorator extends LabelProvider
implements ILightweightLabelDecorator {
@Override
public void decorate(Object element, IDecoration decoration) {
if (element instanceof TextFile) {
TextFileEditor editor = (TextFileEditor) FolderManager
.getWorkbenchPart((TextFile) element);
if (editor != null && editor.isDirty())
decoration.addSuffix("*"); // 節點名稱後綴 *
}
}
public void refreshDecorator(final Object element) {
Display.getDefault().asyncExec(() ->
fireLabelProviderChanged(new LabelProviderChangedEvent(this, element)));
}
}
Editor 修改時通知所有相關方:
public class TextFileEditor extends EditorPart {
@Override
public void doSave(IProgressMonitor monitor) {
dirty = false;
Display.getDefault().asyncExec(() -> {
firePropertyChange(IEditorPart.PROP_DIRTY);
refreshDecoration(); // 通知 Decorator
FolderManager.fireSaveablesDirtyChanged(); // 通知 Navigator
});
}
@Override
public void createPartControl(Composite parent) {
text.addModifyListener(e -> {
dirty = true;
Display.getDefault().asyncExec(() -> {
firePropertyChange(IEditorPart.PROP_DIRTY);
refreshDecoration();
FolderManager.fireSaveablesDirtyChanged();
});
});
}
}
FolderManager 統一廣播:
public class FolderManager {
public static void fireSaveablesDirtyChanged() {
IViewPart view = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getActivePage().findView(SaveableView.ID);
if (view != null) {
Display.getDefault().syncExec(() ->
((SaveableView) view).fireSaveabelsChanged());
}
}
}
SaveablePart 包裝 Editor 實作 Saveable:
public class SaveablePart extends Saveable {
private IWorkbenchPart part;
public SaveablePart(IWorkbenchPart part) { this.part = part; }
public void doSave(IProgressMonitor monitor) {
if (part instanceof ISaveablePart)
((ISaveablePart) part).doSave(monitor);
}
public boolean isDirty() {
return part instanceof ISaveablePart && ((ISaveablePart) part).isDirty();
}
public IWorkbenchPart getWorkbenchPart() { return part; }
}
CNF Saveable Protocol 將「可儲存性」從 Editor 抽離,讓 Navigator View 也能成為儲存操作的入口。開發者只需專注業務模型與 Saveable 映射,框架自動處理狀態同步、UI 更新、儲存動作分發。這不僅提升使用者效率(一次儲存多個編輯器),更符合 Eclipse 「關注點分離」的設計哲學。