實現 LinkHelper
在上個例子中,plugin.xml 中擴展了各類擴展點後,其實並不用我們寫任何 Java 代碼,就能夠在這個 ID 為 com.example.test 的視圖上完成一些 Project Explorer 已實現的操作: 例如創建項目,文件夾,文件等,這些是通過重用 navigatorContent 實現的。
另一個需要注意的地 方是,在我們實現的這個視圖中,IResource 作為與 CNF 的直接接口,同時也扮演著“項目 / 文件夾 / 文 件”這個業務場景的模型角色。(這也是為什麼在 LinkHelper 中 new StructedSelection(file) 能直接定 位到導航器中該節點的原因)
但是在實際的項目開發過程中,並不是每一種業務模型都像文件系統這 樣易於展現。例如 Java 中的 Package 概念就需要以多個文件夾合並成一個樹結點的方式展示(中間以“.” 分隔),jar 包也需要以內部解壓的方式展現其內部樹型結構。
所以通常情況下,業務模型會有單獨 的抽象表示,並作為與 CNF 的直接接口。其優點有:
所有在 Eclipse UI 上對模型的改變,會先傳遞到域模型本身,然後通過 IResource 持久化到操作系統。 對模型的改變操作和改變的傳遞都更直接。如 圖 6所示。
圖 6. Eclipse 中的資源管理
同一套資源文件,通過不同的角度觀察需要有不同的展現,操作及存儲方式。例如一個 Java 項目在 Package Explorer 以更貼近 Java 開發的方式展現與操作,而在 Resource Explorer 則是以操作系統的視角 來展現,顯示所有的文件夾及文件。這裡看待資源的角度,就是構建業務模型所要考慮的事情。
接下來我們會在上一個例子的基礎上,加入自己的業務模型層。雖然在文件和文件夾這個場景下,這一層 的存在意義不大,但是能供讀者在解決具體的工作時借鑒。
我們仿照上個例子建立一個 custom.linkwitheditor.sample 的插件項目,但這次將自己實現大部分的功能,並加入特定的業務模型層。
首先將 view id 更改為 custom.linkwitheditor.view.myCustomNavigatorView,並添加圖標。
刪除 org.eclipse.ui.navigator.viewer 擴展下的 viewerActionBindin —— 這意味著在這個例子我們 並不擴展任何 action,context menus 等。
刪除 org.eclipse.ui.navigator.viewer 擴展下的 viewerContentBinding —— 一會我們會創建自己實 現的 navigatorContent 與 linkHelper。
接下來我們在 Java 代碼中加入自己的模型層,如 圖 7所示。
圖 7. 虛擬的模型層類圖
其 中 VirtualNode 作為與 CNF 的直接接口,ProjectNode,FolderNode,FileNode 均實現此接口。 VirtualNode 的實現代碼如 清單 2所示。(ProjectNode,FolderNode,FileNode 僅需要實現各自的 getImage() 方法,詳見文章附件的源碼)
清單 2. VirtualNode.java
public abstract class VirtualNode { // 內部維持對此虛擬結點對 IResource(Eclipse Core Resource) 的引用 protected IResource resource; // 此虛擬結點的父結點 protected VirtualNode parent; public static final int PROJECT = 1; public static final int FOLDER = 2; public static final int FILE = 3; public void setResource(IResource resource) { this.resource = resource; } public IResource getResource() { return resource; } // 虛擬結點對應的修飾圖像,各實現類均有不同實現 public Image getImage() { return null; } // 虛擬結點顯示的文本,各實現類均有不同實現 public String getText() { return resource.getName(); } public VirtualNode getParent() { return parent; } public void setParent(VirtualNode parent) { this.parent = parent; } }
接下來我們將實現如何將業務模型層傳遞給 CNF 框架展示,其中 MyCustomLabelProvider 比較簡 單,只需要調用相應 VirtualNode 的 getImage() 和 getText(),運行時利用 Java 的多態就會調用各子類 的實現。
清單 3. MyCustomLabelProvider.java
public class MyCustomLabelProvider extends LabelProvider { public Image getImage(Object element) { if (element instanceof VirtualNode) return ((VirtualNode) element).getImage(); return super.getImage(element); } public String getText(Object element) { if (element instanceof VirtualNode) { return ((VirtualNode) element).getText(); } return super.getText(element); } }
MyCustomContentProvider 的設計會復雜一些:
內部維護一個 IResource 和 VirtualNode 之間關聯關系的 hash map,當有新結點生成時注冊至此 hash map。( 另一種方式是實現 Eclipse 內部的 ICommonViewerMapper,並將此 hash map 關聯到視圖上。這樣的好處可以獲得 Eclipse 內部其他事件的通知 )
清單 4. MyCustomContentProvider.java (part 1)
// 內部維護一個 IResource 和 IVirtualNode 相關聯的 hash map // 主要是為了之後擴展 Link With Editor 功能使用 private Map<IResource, VirtualNode> resources2NodesMap; public Map<IResource, VirtualNode> getResources2NodesMap() { return resources2NodesMap; } /** * 以 resource 為 key,virtualNode 為 value,注冊到 resources2NodesMap 中 * @param resource * @param virtualNode */ private void register(IResource resource, VirtualNode virtualNode) { if (register) INSTANCE.resources2NodesMap.put(resource, virtualNode); }
獲得子結點以及創建新結點實現
清單 5. MyCustomContentProvider.java (part 2)
/** * 返回當前結點的子結點,並將子結點納入 resources2NodesMap 的管理 */ public Object[] getChildren(Object element) { List<VirtualNode> projectNodes = new ArrayList<VirtualNode>(); // 如果當前傳入參數為 Eclipse 的根工作空間 if (element instanceof IWorkspaceRoot) { // 遍歷根工作空間下的所有 IProject for (Object o : super.getChildren(element)) { // 根據 IProject 創建相應的 Project 虛擬結點 VirtualNode virtualNode = makeNode(o, null, VirtualNode.PROJECT); projectNodes.add(virtualNode); // 向 resources2NodesMap 注冊當前虛擬結點 register((IResource) o, virtualNode); } } // 如果傳入的是虛擬結點 else if (element instanceof VirtualNode) { VirtualNode node = (VirtualNode) element; for (Object o : super.getChildren(node.getResource())) { // 過濾以 . 開頭的文件和文件夾 if (((IResource) o).getName().startsWith(".")) continue; // 創建並注冊 Folder 虛擬結點 if (o instanceof IFolder) { VirtualNode virtualNode = makeNode(o, node, VirtualNode.FOLDER); projectNodes.add(virtualNode); register((IResource) o, virtualNode); } // 創建並注冊 File 虛擬結點 else if (o instanceof IFile) { VirtualNode virtualNode = makeNode(o, node, VirtualNode.FILE); projectNodes.add(virtualNode); register((IResource) o, virtualNode); } } } // 返回當前傳入元素的子結點 return projectNodes.toArray(); } /** * 創建對應於域模型的虛擬結點 * @param o IResource 對象 * @param parent 父虛擬結點 * @param nodeType 結點類型 * @return 新創建的或已注冊的虛擬結點 */ private VirtualNode makeNode(Object o, VirtualNode parent, int nodeType) { VirtualNode virtualNode = null; if (resources2NodesMap.get(o) == null) { switch (nodeType) { case VirtualNode.PROJECT: virtualNode = new ProjectNode(); break; case VirtualNode.FOLDER: virtualNode = new FolderNode(); break; case VirtualNode.FILE: virtualNode = new FileNode(); break; default: break; } } else { virtualNode = (VirtualNode) resources2NodesMap.get(o); } virtualNode.setResource((IResource) o); virtualNode.setParent(parent); return virtualNode; }
由於 getChildren() 邏輯會將結點注冊,但 hasChildren() 的實現會調用 getChildren()。 所以為了在判斷 N 級結點有沒有子結點,不將子結點注冊,所以用一個局部變量來避免這種情況的發生。
清單 6. MyCustomContentProvider.java (part 3)
// 檢查當前觸發條件是否應將虛擬結點 納入 resources2NodesMap 管理 private boolean register = true; public boolean hasChildren(Object element) { // 加入 register 變量是為了保證在應顯示 N 級結點時,不會由於 hasChildren 的觸發 // 而導致將 N+1 級結點也納入 resources2NodesMap register = false; boolean flag = super.hasChildren(element); register = true; return flag; }
接下來就可以實現針對於業務模層的 linkHelper 實現:其中最關鍵的代碼在於如何處理 resources2NodesMap 為空時如何關聯到導航器結點(例如 workspace 第一次被打開時)。另外業務模型層中 的 parent 也很重要,這關系到在 JFace 內部是否能從葉結點一步步回溯(有興趣的讀者可以試試把 VirtualNode#parent 刪除,做一個對比)。而一些重復代碼的注釋參見 清單 1。
清單 7. MyLinkHelper.java
public class MyLinkHelper implements ILinkHelper { public IStructuredSelection findSelection(IEditorInput anInput) { IFile file = ResourceUtil.getFile(anInput); VirtualNode virtualNode = null; if (file != null) { virtualNode = (VirtualNode) MyCustomContentProvider.INSTANCE .getResources2NodesMap().get(file); // 如果此時 map 未初始化 if (virtualNode == null) { IProject project = file.getProject(); // 1) 模擬展開 project 結點動作 MyCustomContentProvider.INSTANCE.getChildren(MyCustomContentProvider .INSTANCE.getResources2NodesMap().get(project)); // 2) 逐層向上查找 project root IResource parent = file.getParent(); Stack<IResource> stack = new Stack<IResource>(); // 2.1) 將查找過程中的 IResource 對象逐個壓棧 while (parent != project) { stack.push(parent); parent = parent.getParent(); } // 2.2) 逐個 IResource 對象出棧,並模擬展開相應樹結點動作 while (!stack.isEmpty()) { parent = stack.pop(); MyCustomContentProvider.INSTANCE.getChildren(MyCustomContentProvider .INSTANCE.getResources2NodesMap().get(parent)); } // 3) 得到最終的底層結點 virtualNode = (VirtualNode) MyCustomContentProvider.INSTANCE .getResources2NodesMap().get(file); } } return virtualNode != null ? new StructuredSelection(virtualNode) : StructuredSelection.EMPTY; } public void activateEditor(IWorkbenchPage aPage, IStructuredSelection aSelection) { if (aSelection == null || aSelection.isEmpty()) return; if (aSelection.getFirstElement() instanceof FileNode) { IFile file = (IFile) ((FileNode) aSelection.getFirstElement()).getResource(); IEditorInput fileInput = new FileEditorInput(file); IEditorPart editor = null; if ((editor = aPage.findEditor(fileInput)) != null) aPage.bringToTop(editor); } } }
最後我們將已實現的類定義在 plugin.xml。
圖 8. plugin.xml
我們來看一看運 行的效果:
由於我們並沒有在 myCustomNavigatorView 中擴展任何的 action, menu, wizard 等,所以先在 Project Explorer 中創建一些文件。
圖 9. 准備示例所用的文件
切換到 Customized Navigator,點擊 Link With Editor:test1.txt 相對應的導航器結點被選中。
圖 10. 編輯器與導航器結點的關聯
在導航器視圖上切換 test1.txt 結點與 test2.txt 結點,編輯器的當前編輯文件也隨之切換。
總結
Link With Editor 功能運用的范圍非常廣泛,常見的場景除了定位編輯器中打開的各文件, 還有:
利用快捷鍵 (Ctrl+Shift+R/T) 打開文件,定位其在導航器視圖中的位置(例如查看 Eclipse 源碼時)
拖拽導航器視圖中的某結點進入編輯器,這樣必須首先定位該結點在導航器視圖中的位置(例如本文的 圖 7就是這樣自動生成的)
所以在開發 Eclipse 插件應用時,實現此功能是非常必要的。而通過本文第二個例子也可以看出,Link With Editor 功能的實現需要首先理解 CNF 的框架設計思想與實現規范,進而設計業務模型與資源文件間的 映射關系。
樣例代碼
SrcCodePackage.zip