插件開發之:Common Navigator View (CNV) 通用導航試圖

Eclipse提供了非常多的view,從表現形式來說可分爲table view和tree view;從結構上來說可分成三類:Common navigator view, Pagebook view, Task-oriented view。一般情況下,CNV與Resource有關,pagebook跟selection有關,而task-oriented 爲自定義的視圖。基本所有的** explorer都是CNV型的view,基本所有主要插件都有CNV的影子比如IDE,Navigator, Team,JDT, CDT, DTP等。爲什麼要使用CNV? Paper Napkin的文章說的很清楚了,我的看法是,對於怎樣面對大批量+複雜應用的二次抽象(view超多,view內容聯繫超複雜緊密),CNV提供了一個很好的完整實例。

==>> 代碼下載,下載後import到Eclipse 3.4.1+JDK 1.6,run/debug即可。

CNV的視圖特徵:

10分鐘,一個CNV Resource View
  1. 新建一個plugin項目,名字 com.lifesting.hush,將圖標解壓縮到項目下,刷新,打開MANIFEST.MF,在build項裏面將icon目錄鉤上。
  2. 定位Dependencies項,依次加入 org.eclipse.ui.navigator,org.eclipse.ui.navigator.resources,org.eclipse.ui.ide,org.eclipse.jface.text,org.eclipse.ui.editors, org.eclipse.core.resources,org.eclipse.ui.views插件。
  3. 配置一個view extension, 如下圖:


    需要注意的是,這個view的implementation是navigator插件裏面的CommonNavigator,目前我們不需要手寫任何代碼。
  4. 使用Extension Point org.eclipse.ui.navigator.viewer, new一個viewer,viewId設置爲 com.lifesting.hush.view.cnf,popMenuId暫時置空;new一個viewerContentBinding,viewId不變,添加一個includes子節點,然後在其上添加一個contentExtension,屬性pattern爲org.eclipse.ui.navigator.resourceContent,isRoot爲true.
  5. 啓動,點擊菜單Window->Show View->General->Html Explorer,就可以看到效果了,如果view是空白,也不是bug,在左邊的Pakcage Explorer或Resource Explorer新建一個項目,然後關閉Html Explorer再打開,就會看到Html Explorer顯示的和Resource Explorer一模一樣的項目結構。

雖然這個Html Explorer出來了,但設置的org.eclipse.ui.navigator.resourceContent哪來的?怎麼定義的?怎麼添加右鍵菜單?Link爲啥無效?怎樣定製這個顯示?CNF好像也沒有顯著的特點阿?不着急,逐一搞定,從頭開始,最終的效果會是這樣的:


CNV的核心是navigatorContent,所有操作都是圍繞它展開的(可以選擇org.eclipse.ui.navigator.navigatorContent,選擇find references,看看SDK都提供了哪些content),我們這個Html Explorer爲了把過程將的更清楚,將使用兩個自定義的navigatorContent。下面是步驟:

  1. 通過extension point org.eclipse.ui.navigator.navigatorContent 新建一個id爲com.lifesting.cnf.directorycontent的navigatorContent,activatorByDefault=true,LabelProvider=
    org.eclipse.ui.model.WorkbenchLabelProvider,而contentProvider需要新建一個類,非常簡單,就是遍歷IProject或IFolder的子資源(Folder或File)。它的getElement方法實現:
        @Override
        
    public Object[] getElements(Object inputElement) {
            
    if (inputElement instanceof IProject)
            {
                
    try {
                    
    return ((IProject)inputElement).members();
                } 
    catch (CoreException e) {
                    e.printStackTrace();
                }
            }
            
    else if (inputElement instanceof IFolder){
                
    try {
                    
    return ((IFolder)inputElement).members();
                } 
    catch (CoreException e) {
                    e.printStackTrace();
                }
            }
            
    return EMPTY;
        }
    1. 每個navigatorContent都有triggerPoints,很顯然剛纔定義的content通過IProject和IFolder來觸發view tree生成。在這個content下面new 一個triggerPoints,再new兩個instanceof分別指向IProject和IFile。
    2. 在定義actionProvider的時候,需要知道selection大致的類型,在這個content下面new一個possibleChildren,再new一個instanceof 先後IResource(IFile或者IFolder)。

  2. 通過extension point org.eclipse.ui.viewActions給ui view添加一個action用來設置content的Root,它的class如下:
    //bind to mycnfview
    public class OpenDirectoryAction implements IViewActionDelegate {
        
    private MyCnfView view;
        
    public OpenDirectoryAction() {
        }

        @Override
        
    public void init(IViewPart view) {
            
    this.view = (MyCnfView) view;
        }
        @Override
        
    public void run(IAction action) {
            DirectoryDialog dir_dialog 
    = new DirectoryDialog(view.getSite()
                    .getShell());
            String dir_location 
    = retriveSavedDirLocation();
            initDialog(dir_dialog, dir_location);
            String dir 
    = dir_dialog.open();
            
    if (null != dir && !dir.equals(dir_location)) {
                saveDirLocation(dir);
                createPhantomProject(dir);
                fireDirChanged(dir);
            }
        }

        
    private void createPhantomProject(String dir_location) {
            IProject project 
    = ResourcesPlugin.getWorkspace().getRoot().getProject(MyCnfView.PHANTOM_PROJECT_NAME);
            
    // 1 delete previous defined project
            if (project.exists()) {
                
    try {
                    project.delete(
    falsetruenull);
                } 
    catch (CoreException e) {
                    e.printStackTrace();
                }
            }
            
    // 2 create new project with the same name
            final IProjectDescription desc = ResourcesPlugin.getWorkspace().newProjectDescription(MyCnfView.PHANTOM_PROJECT_NAME);
            desc.setLocationURI(
    new File(dir_location).toURI());
            IRunnableWithProgress op 
    = new IRunnableWithProgress() {
                
    public void run(IProgressMonitor monitor)
                        
    throws InvocationTargetException {
                    CreateProjectOperation op 
    = new CreateProjectOperation(desc,
                            
    "Build Algorithm Library");
                    
    try {
                        PlatformUI.getWorkbench().getOperationSupport()
                                .getOperationHistory().execute(
                                        op,
                                        monitor,
                                        WorkspaceUndoUtil
                                                .getUIInfoAdapter(view.getSite().getShell()));
                    } 
    catch (ExecutionException e) {
                        
    throw new InvocationTargetException(e);
                    }
                }
            };
            
    try {
                view.getSite().getWorkbenchWindow().run(
    falsefalse, op);
            } 
    catch (InvocationTargetException e) {
                e.printStackTrace();
            } 
    catch (InterruptedException e) {
                e.printStackTrace();
            }
            
    // 3 add the new created project to default workingset
            if (project.exists()) {
                view.getSite().getWorkbenchWindow().getWorkbench().getWorkingSetManager().addToWorkingSets(project,
                        
    new IWorkingSet[] {});
                
    //4 waiting the project is ready(file structure is built)
                try {
                    project.refreshLocal(IResource.DEPTH_INFINITE, 
    null);
                } 
    catch (CoreException e) {
                    e.printStackTrace();
                }
            }  
        }

       
    //...略..輔助方法
    }
    代碼要表達的就是建立一個隱含的project,將action取得的directory下所有的文件都倒入到項目中來

  3. 將ui view的class從CommonNavigator變爲一個它的子類MyCnfView:
    public class MyCnfView extends CommonNavigator {

        
    public static final String KEY_DIR_LOCATION="com.lifesting.cnf.myview_location";
        
    public static final String PHANTOM_PROJECT_NAME=".htmlproject";
        
    public MyCnfView() {
        }
        
    public IAdaptable getProjectInput(){
            IWorkspaceRoot ws_root 
    = ResourcesPlugin.getWorkspace().getRoot();
            IProject proj 
    = ws_root.getProject(PHANTOM_PROJECT_NAME);
            
    if (!proj.exists()) return getSite().getPage().getInput();
            
    return proj;
        }
        
    public void reset()
        {
            getCommonViewer().setInput(getProjectInput());
            getCommonViewer().refresh();
        }
        @Override
        
    protected IAdaptable getInitialInput() {
            
    return getProjectInput();
        }

    }

  4. 將viewerContentBinding/includes的contentExtension的pattern替換爲剛纔定義的com.lifesting.cnf.directorycontent。

  5. 因爲是Html Explorer,需要過濾掉非html文件,需要設置一個過濾器。通過extension point org.eclipse.ui.navigator.navigatorContent 新建一個id爲com.lifesting.cnf.filter.nothtml的filter,它的class非常簡單:
    public class NotHtmlFilter extends ViewerFilter {

        
    public NotHtmlFilter() {
        }

        @Override
        
    public boolean select(Viewer viewer, Object parentElement, Object element) {
            
    if (element instanceof IFile)
            {
                
    return Util.isHtmlFile((IFile)element);
            }
            
    return true;
        }
    }
    再將此filter配置到cnv的viewerContentBinding/includes中去,跟contentExtension配置過程一樣。
  6. 啓動後,cnv已經可以工作,爲了演示navigatorContent的可重複利用性,再定義一個只包含html文檔標題的html title content(爲方便只掃描標題),掛在前面定義的directory content上。directory content的model是IProject/IFile/IFolder,html title content需要定義一個model,一個html文檔掃描器,還有contentPrvoider和lableProvider。
    • model
      public class HeadTitle {
          
      private String title;
          
      private IFile file;
          
      private int from = 0;
          
      public int getFrom() {
              
      return from;
          }
              
      //略set/get
      }
    • scaner
          public static HeadTitle parse(InputStream in) throws IOException {
              BufferedReader br 
      = new BufferedReader(new InputStreamReader(in));
              
      int c = -1;
              StringBuffer sb 
      = new StringBuffer();
              
      boolean tag = false;
              
      boolean found_title = false;
              String to_match 
      = "title";
              HeadTitle title 
      = new HeadTitle();
              
      int counter = 0;
              
      int start = 0;
              outer: 
      while ((c = br.read()) != -1) {
                  
      if (c == '<') {
                      br.mark(
      3);
                      
      if (br.read() == '!' && br.read() == '-' && br.read() == '-') {
                          
      // loop over html comment until -->
                          counter += 3;
                          
      int t1, t2, t3;
                          t1 
      = t2 = t3 = 0;
                          
      while ((c = br.read()) != -1) {
                              t3 
      = t2;
                              t2 
      = t1;
                              t1 
      = c;
                              counter
      ++;
                              
      if (t3 == '-' && t2 == '-' && t1 == '>') {
                                  counter
      ++// '<' also need be countered
                                  continue outer;
                              }
                          }
                          
      break outer; //reach the end
                      } else {
                          br.reset();
                      }
                      tag 
      = true;
                      
      if (found_title) {
                          title.setTitle(sb.toString());
                          title.setFrom(start);
                          title.setTo(counter);
                          
      return title;
                      }
                  } 
      else if (c == '>') {
                      start 
      = counter + 1;
                      
      if (tag) {
                          String s 
      = sb.toString().trim();
                          found_title 
      = to_match.equalsIgnoreCase(s);
                          sb.setLength(
      0);
                          tag 
      = false;
                      }
                  } 
      else {
                      sb.append((
      char) c);
                  }
                  counter
      ++;
              }
              title.setTitle(
      "No title");
              
      return title;
          }
    • contentProvider只有一個getChildren比較重要
          private static final Object[] EMPTY = new Object[0];
          @Override
          
      public Object[] getChildren(Object parentElement) {
              
      if (parentElement instanceof IFile)
              {
                  IFile f 
      = (IFile) parentElement;
                  
      if(Util.isHtmlFile(f))
                  {
                      
      try {
                          HeadTitle head 
      = SimpleHtmlParser.parse(f.getContents());
                          head.setFile(f);
                          
      return new HeadTitle[]{head};
                      } 
      catch (IOException e) {
                          e.printStackTrace();
                      } 
      catch (CoreException e) {
                          e.printStackTrace();
                      }
                  }
              }
              
      return EMPTY;
          }
    • labelProivder
      public class HtmlTitleLabelProvider extends LabelProvider {
          
      public static final String KEY_TITLE_IMAGE="icon/title.GIF";
          @Override
          
      public String getText(Object element) {
              
      if (element instanceof HeadTitle)
                  
      return ((HeadTitle)element).getTitle();
              
      else if (element instanceof IFile)
                  
      return ((IFile)element).getName();
              
      return super.getText(element);
          }
          @Override
          
      public Image getImage(Object element) {
              
      if (element instanceof HeadTitle)
              {
                  
                  Image img 
      = Activator.getDefault().getImageRegistry().get(KEY_TITLE_IMAGE);
                  
      if (img == null)
                  {
                      Activator.getDefault().getImageRegistry().put(KEY_TITLE_IMAGE, (img 
      = Activator.imageDescriptorFromPlugin(Activator.PLUGIN_ID, KEY_TITLE_IMAGE).createImage()));
                  }
                  
      return img;            
              }
              
      return super.getImage(element);
          }
      }

  7. html title content利用directory content找到文件,提取標題,但二者有個東西來觸發這個過程。在html title content下定義個一個triggerPoints,使用instanceof=IFile來觸發。
  8. 所有的功能基本完成,剩下popmenu和link,popmenu可以有兩種方式, contribute或cnv下的popmenu子節點.contribute會在popmenu下建一堆比如group.*的menu placeholder。content下可以配置actionProvider來完成popmenu的功能,爲簡單隻在popmenu上放置一個open的動作,即open html file,如果是html file,直接打開;如果是html file title,還須將html title高亮顯示,以示不通,actionProivder:
    public class MyCommonActionProvider extends CommonActionProvider {

        
    private IAction action;
        
    public MyCommonActionProvider() {
            
        }
        @Override
        
    public void init(ICommonActionExtensionSite site) {
            
    super.init(site);
            ICommonViewerSite check_site 
    = site.getViewSite();
            
    if (check_site instanceof ICommonViewerWorkbenchSite)
            {
                ICommonViewerWorkbenchSite commonViewerWorkbenchSite 
    = (ICommonViewerWorkbenchSite)check_site;
                action 
    = new OpenFileAction(commonViewerWorkbenchSite.getPage(),commonViewerWorkbenchSite.getSelectionProvider());
            }
        }
        @Override
        
    public void fillActionBars(IActionBars actionBars) {
            
    super.fillActionBars(actionBars);
            actionBars.setGlobalActionHandler(ICommonActionConstants.OPEN, action);
        }
        @Override
        
    public void fillContextMenu(IMenuManager menu) {
            
    super.fillContextMenu(menu);
            
    if (action.isEnabled())
                menu.appendToGroup(
    "group.edit", action);
        }
    }
    open file action:
    public class OpenFileAction extends Action {

        
    private IWorkbenchPage page;
        
    private ISelectionProvider provider;
        
    private Object selected = null;
        
        
    public OpenFileAction(IWorkbenchPage page,
                ISelectionProvider selectionProvider) {
            
    this.page = page;
            
    this.provider = selectionProvider;
            setText(
    "Open");
            setDescription(
    "Doo");
            setImageDescriptor(Activator.imageDescriptorFromPlugin(Activator.PLUGIN_ID, 
    "icon/lookin.GIF"));
        }
        @Override
        
    public boolean isEnabled() {
            ISelection selection 
    = provider.getSelection();
            
    if(!selection.isEmpty())
            {
                IStructuredSelection structuredSelection 
    = (IStructuredSelection)selection;
                Object element 
    = structuredSelection.getFirstElement();
                selected 
    = element;
                
    return element instanceof IFile || element instanceof HeadTitle;
            }
            selected 
    = null;
            
    return false;
        }
        @Override
        
    public void run() {
            
    if (null == selected) return ;
            IFile file 
    = ((selected instanceof HeadTitle) ? ((HeadTitle)selected).getFile() : (IFile)selected);
            FileEditorInput fileEditInput 
    = new FileEditorInput(file);
            
    try {
                TextEditor editor 
    = (TextEditor) page.openEditor(fileEditInput, "org.eclipse.ui.DefaultTextEditor");
                
    if (selected instanceof HeadTitle)
                {
                    
    int from = ((HeadTitle)selected).getFrom();
                    
    int to = ((HeadTitle)selected).getTo();
                    editor.selectAndReveal(from, to
    -from);
                }
            } 
    catch (PartInitException e) {
                e.printStackTrace();
            }
        }
        
    }
  9. Link功能非常簡單,使用extension point org.eclipse.ui.navigator.linkHelper,它有兩個子節點selectionEnablement和editorinputEnablement,分別對應在view中的selection和打開editor中的editorInput,class爲:
    public class SimpleHtmlLinkHelper implements ILinkHelper {

        @Override
        
    public void activateEditor(IWorkbenchPage page,
                IStructuredSelection selection) {
            Object obj 
    = selection.getFirstElement();
            
    if (obj instanceof IFile)
            {
                FileEditorInput input 
    = new FileEditorInput((IFile) obj);
                IEditorPart editor 
    = page.findEditor(input);
                
    if(editor != null)
                {
                    page.bringToTop(editor);
                }
            }
        }

        @Override
        
    public IStructuredSelection findSelection(IEditorInput anInput) {
            
    if (anInput instanceof IFileEditorInput)
            {
                IFile file 
    = ((IFileEditorInput)anInput).getFile();
                StructuredSelection selection 
    = new StructuredSelection(file);
                
    return selection;
            }
            
    return null;
        }

    }
插件太複雜,不適合一篇blog講清楚,如果有人對cnv有些比明白,歡迎來郵件討論。
發佈了253 篇原創文章 · 獲贊 4 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章