插件开发之:Common Navigator View (CNV) 通用导航试图

插件开发之: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(
    false true null );
                } 
    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(
    false false , 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 ;
        }
    }
  6. 再将此filter配置到cnv的viewerContentBinding/includes中去,跟contentExtension配置过程一样。
  7. 启动后,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);
          }
      }

  8. html title content利用directory content找到文件,提取标题,但二者有个东西来触发这个过程。在html title content下定义个一个triggerPoints,使用instanceof=IFile来触发。
  9. 所有的功能基本完成,剩下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();
            }
        }
        
    }
  10. 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有些比明白,欢迎来邮件讨论。

你可能感兴趣的:(插件开发之:Common Navigator View (CNV) 通用导航试图)