Eclipse提供了非常多的view,從表現(xiàn)形式來(lái)說(shuō)可分為table view和tree view;從結(jié)構(gòu)上來(lái)說(shuō)可分成三類(lèi):Common navigator view, Pagebook view, Task-oriented view。一般情況下,CNV與Resource有關(guān),pagebook跟selection有關(guān),而task-oriented 為自定義的視圖?;舅械?* explorer都是CNV型的view,基本所有主要插件都有CNV的影子比如IDE,Navigator, Team,JDT, CDT, DTP等。為什么要使用CNV?
Paper Napkin的文章說(shuō)的很清楚了,我的看法是,對(duì)于怎樣面對(duì)大批量+復(fù)雜應(yīng)用的二次抽象(view超多,view內(nèi)容聯(lián)系超復(fù)雜緊密),CNV提供了一個(gè)很好的完整實(shí)例。
==>>
代碼下載,下載后import到Eclipse 3.4.1+JDK 1.6,run/debug即可。
CNV的視圖特征:
10分鐘,一個(gè)CNV Resource View
- 新建一個(gè)plugin項(xiàng)目,名字 com.lifesting.hush,將圖標(biāo)解壓縮到項(xiàng)目下,刷新,打開(kāi)MANIFEST.MF,在build項(xiàng)里面將icon目錄鉤上。
- 定位Dependencies項(xiàng),依次加入 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插件。
- 配置一個(gè)view extension, 如下圖:

需要注意的是,這個(gè)view的implementation是navigator插件里面的CommonNavigator,目前我們不需要手寫(xiě)任何代碼。
- 使用Extension Point org.eclipse.ui.navigator.viewer, new一個(gè)viewer,viewId設(shè)置為 com.lifesting.hush.view.cnf,popMenuId暫時(shí)置空;new一個(gè)viewerContentBinding,viewId不變,添加一個(gè)includes子節(jié)點(diǎn),然后在其上添加一個(gè)contentExtension,屬性pattern為org.eclipse.ui.navigator.resourceContent,isRoot為true.
- 啟動(dòng),點(diǎn)擊菜單Window->Show View->General->Html Explorer,就可以看到效果了,如果view是空白,也不是bug,在左邊的Pakcage Explorer或Resource Explorer新建一個(gè)項(xiàng)目,然后關(guān)閉Html Explorer再打開(kāi),就會(huì)看到Html Explorer顯示的和Resource Explorer一模一樣的項(xiàng)目結(jié)構(gòu)。
雖然這個(gè)Html Explorer出來(lái)了,但設(shè)置的org.eclipse.ui.navigator.resourceContent哪來(lái)的?怎么定義的?怎么添加右鍵菜單?Link為啥無(wú)效?怎樣定制這個(gè)顯示?CNF好像也沒(méi)有顯著的特點(diǎn)阿?不著急,逐一搞定,從頭開(kāi)始,最終的效果會(huì)是這樣的:
CNV的核心是navigatorContent,所有操作都是圍繞它展開(kāi)的(可以選擇org.eclipse.ui.navigator.navigatorContent,選擇find references,看看SDK都提供了哪些content),我們這個(gè)Html Explorer為了把過(guò)程將的更清楚,將使用兩個(gè)自定義的navigatorContent。下面是步驟:
- 通過(guò)extension point org.eclipse.ui.navigator.navigatorContent 新建一個(gè)id為com.lifesting.cnf.directorycontent的navigatorContent,activatorByDefault=true,LabelProvider=
org.eclipse.ui.model.WorkbenchLabelProvider,而contentProvider需要新建一個(gè)類(lèi),非常簡(jiǎn)單,就是遍歷IProject或IFolder的子資源(Folder或File)。它的getElement方法實(shí)現(xiàn):
@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;
}
- 每個(gè)navigatorContent都有triggerPoints,很顯然剛才定義的content通過(guò)IProject和IFolder來(lái)觸發(fā)view tree生成。在這個(gè)content下面new 一個(gè)triggerPoints,再new兩個(gè)instanceof分別指向IProject和IFile。
- 在定義actionProvider的時(shí)候,需要知道selection大致的類(lèi)型,在這個(gè)content下面new一個(gè)possibleChildren,再new一個(gè)instanceof 先后IResource(IFile或者IFolder)。
- 通過(guò)extension point org.eclipse.ui.viewActions給ui view添加一個(gè)action用來(lái)設(shè)置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();
}
}
}
//...略..輔助方法
}
代碼要表達(dá)的就是建立一個(gè)隱含的project,將action取得的directory下所有的文件都倒入到項(xiàng)目中來(lái)。
- 將ui view的class從CommonNavigator變?yōu)橐粋€(gè)它的子類(lèi)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();
}
}
- 將viewerContentBinding/includes的contentExtension的pattern替換為剛才定義的com.lifesting.cnf.directorycontent。
- 因?yàn)槭荋tml Explorer,需要過(guò)濾掉非html文件,需要設(shè)置一個(gè)過(guò)濾器。通過(guò)extension point org.eclipse.ui.navigator.navigatorContent 新建一個(gè)id為com.lifesting.cnf.filter.nothtml的filter,它的class非常簡(jiǎn)單:
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配置過(guò)程一樣。
- 啟動(dòng)后,cnv已經(jīng)可以工作,為了演示navigatorContent的可重復(fù)利用性,再定義一個(gè)只包含html文檔標(biāo)題的html title content(為方便只掃描標(biāo)題),掛在前面定義的directory content上。directory content的model是IProject/IFile/IFolder,html title content需要定義一個(gè)model,一個(gè)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只有一個(gè)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);
}
}
- html title content利用directory content找到文件,提取標(biāo)題,但二者有個(gè)東西來(lái)觸發(fā)這個(gè)過(guò)程。在html title content下定義個(gè)一個(gè)triggerPoints,使用instanceof=IFile來(lái)觸發(fā)。
- 所有的功能基本完成,剩下popmenu和link,popmenu可以有兩種方式, contribute或cnv下的popmenu子節(jié)點(diǎn).contribute會(huì)在popmenu下建一堆比如group.*的menu placeholder。content下可以配置actionProvider來(lái)完成popmenu的功能,為簡(jiǎn)單只在popmenu上放置一個(gè)open的動(dòng)作,即open html file,如果是html file,直接打開(kāi);如果是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();
}
}
}
- Link功能非常簡(jiǎn)單,使用extension point org.eclipse.ui.navigator.linkHelper,它有兩個(gè)子節(jié)點(diǎn)selectionEnablement和editorinputEnablement,分別對(duì)應(yīng)在view中的selection和打開(kāi)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;
}
}
插件太復(fù)雜,不適合一篇blog講清楚,如果有人對(duì)cnv有些比明白,歡迎來(lái)郵件討論。