HadoopIntellijPlugin插件HDFS文件系统浏览器设计和实现2

  本节,将详细说明一下文件系统树UI层的展示设计和实现。文件系统对象以树的方式展示,在IntelliJ 框架内,文件树依附于浮动面板,即IntelliJ 的ToolWindow 插件。UI需要有层次感,IntelliJ 的 ToolWindow 作为文件系统UI的最底层,由IDEA 框架去维护和控制;在ToolWindow上面,需要有个主窗体ToolWindowForm,该窗体上将会呈现文件系统Tree的UI和文件系统对象的相关属性UI;由于可能存在多个连接,连接到HDFS,因此会存在多个文件系统Tree,因此文件系统Tree 的UI将分为两种方式呈现,一种以Tab列表的方式平铺,另一种,以单个树根节点的方式呈现,因此,需要有TabbedBrowsForm 和 SimpleBrowserForm 两种UI,在这两种UI之上,才真正显示出文件系统的Tree控件。因此本节也将从这几个方面来介绍。
文件系统树UI整体设计的类图如下:

文件系统树FileSystemBrowserTree及其渲染类FileSystemBrowserTreeCellRenderer

  FileSystemBrowserTree 类为树的控件类,继承了 FileSystemTree 类,FileSystemTree继承了IDEA本身的Tree。FileSystemBrowserTree 类定义了对文件系统Tree的相关操作,包括获取TreeModel、进行树节点TreeNode的导航(前一节点、后一节点)、树节点TreeNode的折叠和展开、鼠标悬停TreeNode的提示文字、获取选中的TreeNode、TreeNode选中的事件通知、TreeNode的鼠标事件、TreeNode的键盘事件等。下面就Tree的初始化和右键菜单做详细说明。

FileSystemBrowserTree的初始化方法

  初始化中包括鼠标、键盘、选择事件的注册、设置根节点的可见性、设置Tree的相关滚动条、设置Tree渲染方式,初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 初始化树控件
* @param treeModel
*/
public FileSystemBrowserTree(BrowserTreeModel treeModel)
{
super(treeModel);
//注册监听事件
addKeyListener(this.keyListener);
addMouseListener(this.mouseListener);
addTreeSelectionListener(this.treeSelectionListener);
setToggleClickCount(0);
setRootVisible(treeModel instanceof TabbedBrowserTreeModel);
setShowsRootHandles(true);
setAutoscrolls(true);
//设置TreeNode的渲染器
FileSystemBrowserTreeCellRenderer browserTreeCellRenderer = new FileSystemBrowserTreeCellRenderer(treeModel.getProject());
setCellRenderer(browserTreeCellRenderer);
FileSystemBrowserTreeSpeedSearch speedSearch = new FileSystemBrowserTreeSpeedSearch(this);
//注册相关需要释放的资源对象
Disposer.register(this, speedSearch);
Disposer.register(this, treeModel);
Disposer.register(this, this.navigationHistory);
}

TreeNode的右键菜单设计

  当选中树的某个节点,根据其节点类型,显示不同的右键菜单项。目前节点只有两种类型,一是文件系统对象的集合类型,即FIleSystemObjectBundle,该类型特指整个HDFS根目录下(“/“)的所有对象(包括目录和文件);二是文件系统的对象,目录或者文件,这里为简单起见,文件不区分具体的类型。每种类型显示哪些右键菜单,在本节后面介绍。右键菜单展示,需要在TreeNode的鼠标选中节点右键释放后处理。在处理过程中使用了多线程操作。定义内部类MouseListener 封装了 整个鼠标事件处理,其中方法 mouseReleased(final MouseEvent event) 实现了右键菜单的展示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
*鼠标释放事件
* @param event
*/
public void mouseReleased(final MouseEvent event)
{
if (event.getButton() == 3)
{
TreePath path = FileSystemBrowserTree.this.getPathForLocation(event.getX(), event.getY());
if (path != null)
{
final FileSystemBrowserTreeNode lastPathEntity = (FileSystemBrowserTreeNode) path.getLastPathComponent();
if (lastPathEntity.isDisposed())
{
return;
}
//开启一个右键菜单判断操作线程
new ModalTask(lastPathEntity.getProject(), "Loading fsobject information", true)
{
//进行TreeNode的类型判断,并实例化菜单的Action
protected void execute(@NotNull ProgressIndicator progressIndicator)
{
ActionGroup actionGroup = null;
if ((lastPathEntity instanceof FileSystemObject))
{
//文件系统对象节点,右键菜单
FileSystemObject object = (FileSystemObject) lastPathEntity;
actionGroup = new ObjectActionGroup(object);
}
else if ((lastPathEntity instanceof FileSystemObjectBundle))
{
//文件系统对象集合节点
FileSystemObjectBundle objectsBundle = (FileSystemObjectBundle) lastPathEntity;
ConnectionHandler connectionHandler = objectsBundle.getConnectionHandler();
actionGroup = new ConnectionActionGroup(connectionHandler);
}
if ((actionGroup != null) && (!progressIndicator.isCanceled()))
{
//开启显示右键菜单操作线程
ActionPopupMenu actionPopupMenu = ActionManager.getInstance().createActionPopupMenu("", actionGroup);
popupMenu = actionPopupMenu.getComponent();
new SimpleLaterInvocator()
{
protected void execute()
{
if (FileSystemBrowserTree.this.isShowing())
{
popupMenu.show(FileSystemBrowserTree.this,event.getX(),event.getY());
}
}
}.start();
} else
{
FileSystemBrowserTree.this.popupMenu = null;
}
}
}.start();
}
}
}
};

TreeNode的渲染器类 FileSystemBrowserTreeCellRenderer

  如果要改变TreeNode的显示方式要使用到TreeCellRender,通过对它的实现才能够得到不同显示方式的Tree。这里自定义继承 TreeCellRender 类,实现了TreeNode 节点的图标显示、文字显示等。在FileSystemBrowserTreeCellRenderer 类中重写了 Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) 方法。该方法返回一个Component这个控件,也就是你要设置的树中节点的显示风格,当然在实现的时候你可以继承一个Jcomponent类的子类,也可以在类中设置一个私有变量然后返回。
  在这个方法中有很多参数,首先是Jtree 这个就是你要设置的树,对应的对象,然后是value ,这个其实是节点,通过他你可以获得节点的数据,以及对应的子节点父节点等。接着是selected表示如果被选中时该如何显示。Expanded则表示如果出于扩展状态如何显示节点,然后是leaf,叶子节点的显示方式可以通过这个条件设置;row这个参数,如果有伸缩的话,row是随时改变的,最后一个hasFocus是是否拥有焦点,设置拥有焦点时的显示方式。实现了这个方法后,用setCellRende()方法设置一下这个类的实例就行了。详细可参见该类的代码…./src/main/java/com/fangyuzhong/intelliJ/hadoop/fsbrowser/ui/FileSystemBrowserTreeCellRenderer.java

文件系统主窗体设计

  文件系统主窗体UI设计分为三个部分,一个是ToolWindow 上呈现的UI:BrowserToolWindowForm,该窗体为容器,这个窗体上,将构建Tree的显示UI和文件系统对象的属性显示UI;第二个是 展现Tree的UI窗体TabbedBrowserForm和SimpleBrowserForm;第三部分是文件对象属性展示UI:ObjectPropertiesForm。三部分都是UI界面,IDEA开发UI界面使用 GUI Form的开发方式。主界面UI采用XML来描述,类的文件名后面加”.form”构成,UI界面可以使用控件进行拖拽,UI背后的业务逻辑 使用和该类同名的java 文件描述。IDEA这么做,无非就是想摆脱繁琐的Swing代码,微软的WPF技术也是同样使用这种方法。

展现Tree的UI窗体TabbedBrowserForm和SimpleBrowserForm

  当用户选择以Tab列表的方式展现多个HDFS连接,那么使用TabbedBrowserForm来展示,当用户选择以单个树控件多个根节点方式展现多个HDFS连接,那么使用SimpleBrowserForm方式来展示,TabbedBrowserForm展示方法借助于SimpleBrowserForm,两个窗体都继承抽象FileSystemBrowserFrom。
  抽象类FileSystemBrowserFrom 继承了项目中自定义的窗体基类。在FileSystemBrowserFrom 中有三个重要的抽象方法:
①、public abstract FileSystemBrowserTree getBrowserTree() :获取文件系统树Tree控件对象
②、public abstract void selectElement(FileSystemBrowserTreeNode paramBrowserTreeNode, boolean paramBoolean1, boolean paramBoolean2); 设置选中的Tree的元素
③、public abstract void rebuildTree(); 构建Tree控件
三个抽象方法均在TabbedBrowserForm和SimpleBrowserForm 中具体实现。

SimpleBrowserForm类

  该类比较简单,在UI上,有个主面板Jpanel ,放置FileSystemBrowserTree控件,这是UI最基础的部分,Tree控件始终是在这个UI上的,TabbedBrowserForm也是借助该窗体进行展示,只不过会根据HDFS连接个数 new 这个窗体,放到Tab页上而已。使用SimpleBrowserTreeModel实例化一个FileSystemBrowserTree控件,初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 使用SimpleBrowserTreeModel,初始化
* @param parentComponent
*/
public SimpleBrowserForm(DisposableProjectComponent parentComponent)
{
this(parentComponent, new SimpleBrowserTreeModel(parentComponent.getProject(), ConnectionManager.getInstance(parentComponent.getProject()).getConnectionBundle()));
}
/**
* TabbedBrowserTreeModel,初始化
* @param parentComponent
* @param connectionHandler
*/
public SimpleBrowserForm(DisposableProjectComponent parentComponent, ConnectionHandler connectionHandler)
{
this(parentComponent, new TabbedBrowserTreeModel(connectionHandler));
}
/**
*初始化,使用BrowserTreeModel 创建文件树控件对象FileSystemBrowserTree
* @param parentComponent
* @param treeModel
*/
private SimpleBrowserForm(DisposableProjectComponent parentComponent, BrowserTreeModel treeModel)
{
super(parentComponent);
this.browserTree = new FileSystemBrowserTree(treeModel);
this.browserScrollPane.setViewportView(this.browserTree);
this.browserScrollPane.setBorder(new EmptyBorder(1, 0, 0, 0));
ToolTipManager.sharedInstance().registerComponent(this.browserTree);
Disposer.register(this, this.browserTree);
}

重写了抽象类的3个抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 获取FileSystemBrowserTree对象
* @return
*/
public FileSystemBrowserTree getBrowserTree()
{
return this.browserTree;
}
/**
* 设置选中树元素
*/
public void selectElement(FileSystemBrowserTreeNode treeNode, boolean focus, boolean scroll)
{
this.browserTree.selectElement(treeNode, focus);
}
/**
* 构建Tree
*/
public void rebuildTree()
{
this.browserTree.getModel().getRoot().rebuildTreeChildren();
}

TabbedBrowserForm类

  该类,将用户创建的多个HDFS连接,以Tab页(列表)的方式呈现。因此界面UI有两个控件,一个是主面板mainPanel,一个是自定义开发 tab 控件 TabbedPane 。

该类的初始化。

  首先创建一个TabbedPane 控件,注册相关事件监听器;然后调用initTabs()方法根据连接数创建各个连接的Tab页。initTabs()方法中,通过HDFS连接管理类,获取当前HDFS连接集合,然后遍历连接集合,读取每个连接,创建一个Tab页面,并且实例化一个SimpleBrowserForm窗体,放到tab页面上;最后,将TabbedPane 加入到主面板中,完成整个UI的构建。初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 初始化
* @param parentComponent
*/
public TabbedBrowserForm(BrowserToolWindowForm parentComponent)
{
super(parentComponent);
this.connectionTabs = new TabbedPane(this);
initTabs();
this.connectionTabs.addListener(new TabsListener() {
public void selectionChanged(TabInfo oldSelection, TabInfo newSelection){}
public void beforeSelectionChanged(TabInfo oldSelection, TabInfo newSelection){}
public void tabRemoved(TabInfo tabInfo) {}
public void tabsMoved(){}});
Disposer.register(this, this.connectionTabs);
}

/**
* 初始化Tab 列表
*/
private void initTabs()
{
Project project = getProject();
TabbedPane oldConnectionTabs = this.connectionTabs;
this.connectionTabs = new TabbedPane(this);
ConnectionManager connectionManager = ConnectionManager.getInstance(project);
//获取HDFS连接集合,遍历连接集合,读取每个连接,创建一个Tab页面
ConnectionBundle connectionBundle = connectionManager.getConnectionBundle();
for (ConnectionHandler connectionHandler : connectionBundle.getConnectionHandlers())
{
//根据每个连接,实例化一个 SimpleBrowserForm,放到一个新的Tab页上
SimpleBrowserForm browserForm = new SimpleBrowserForm(this, connectionHandler);
JComponent component = browserForm.getComponent();
TabInfo tabInfo = new TabInfo(component);
tabInfo.setText(CommonUtil.nvl(connectionHandler.getName(), "[unnamed fsconnection]"));
tabInfo.setObject(browserForm);
this.connectionTabs.addTab(tabInfo);
}
//添加connectiontab 到主面板控件mainPanel
if (this.connectionTabs.getTabCount() == 0)
{
this.mainPanel.removeAll();
this.mainPanel.add(new JBList(new ArrayList()), "Center");
}
else if (this.mainPanel.getComponentCount() > 0)
{
Component component = this.mainPanel.getComponent(0);
if (component != this.connectionTabs)
{
this.mainPanel.removeAll();
this.mainPanel.add(this.connectionTabs, "Center");
}
}
else
{
this.mainPanel.add(this.connectionTabs, "Center");
}
DisposerUtil.dispose(oldConnectionTabs);
}
重写基类的抽象方法

  由于是以Tab列表的方式展现Tree,在任何时候,只有一个Tab页是激活的,因此,在获取FileSystemBrowserTree对象,有两种方法,一是获取当前激活的Tab上的Tree,二是,获取指定HDFS连接所在的Tree。选中树元素和构建树,就可以调用SimpleBrowserForm 的相关方法了。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 获取当前活动的Tree
* @return
*/
@Nullable
public FileSystemBrowserTree getBrowserTree()
{
return getActiveBrowserTree();
}
/**
* 获取指定连接的Tree
* @param connectionHandler
* @return
*/
@Nullable
public FileSystemBrowserTree getBrowserTree(ConnectionHandler connectionHandler)
{
SimpleBrowserForm browserForm = getBrowserForm(connectionHandler);
return browserForm == null ? null : browserForm.getBrowserTree();
}
/**
* 获取当前激活的Tree
* @return
*/
@Nullable
public FileSystemBrowserTree getActiveBrowserTree()
{
TabInfo tabInfo = this.connectionTabs.getSelectedInfo();
if (tabInfo != null)
{
SimpleBrowserForm browserForm = (SimpleBrowserForm) tabInfo.getObject();
return browserForm.getBrowserTree();
}
return null;
}
/**
* 设置选中元素
* @param treeNode
* @param focus
* @param scroll
*/
public void selectElement(FileSystemBrowserTreeNode treeNode, boolean focus, boolean scroll)
{
ConnectionHandler connectionHandler = treeNode.getConnectionHandler();
SimpleBrowserForm browserForm = getBrowserForm(connectionHandler);
if (browserForm != null)
{
this.connectionTabs.select(browserForm.getComponent(), focus);
if (scroll)
{
browserForm.selectElement(treeNode, focus, scroll);
}
}
}
/**
* 构建树
*/
public void rebuildTree()
{
for (TabInfo tabInfo : this.connectionTabs.getTabs())
{
SimpleBrowserForm browserForm = (SimpleBrowserForm) tabInfo.getObject();
browserForm.rebuildTree();
}
}

主窗体UI,BrowserToolWindowForm。

   该类,将Tree的UI和文件系统对象属性UI及其工具栏的Action组合起来。界面UI上,有个主面板 mainPanel、存放FileSystemBrowserForm面板 browserPanel、存放ObjectProperties的面板 objectPropertiesPanel。在初始化中,先注册显示方式设置的监听,然后调用 rebuild()方法,构建Tree 的UI组件;最后添加工具栏的Action,初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 初始化
* @param project
*/
public BrowserToolWindowForm(Project project)
{
super(project);

/*
注册显示方式设置监听
*/
this.displayModeSettingsListener = new DisplayModeSettingsListener()
{
public void displayModeChanged(BrowserDisplayMode displayMode)
{
if (BrowserToolWindowForm.this.getDisplayMode() != displayMode)
{
BrowserToolWindowForm.this.setDisplayMode(displayMode);
BrowserToolWindowForm.this.rebuild();
}
}
};
//构建UI
rebuild();
//添加工具栏
ActionToolbar actionToolbar = ActionUtil.createActionToolbar("", true,
"HadoopNavigator.ActionGroup.Browser.Controls");
actionToolbar.setTargetComponent(this.actionsPanel);
this.actionsPanel.add(actionToolbar.getComponent());
//添加文件对象属性显示UI
this.objectPropertiesPanel.setVisible(true);
this.objectPropertiesForm = new ObjectPropertiesForm(this);
this.objectPropertiesPanel.add(this.objectPropertiesForm.getComponent());
GuiUtils.replaceJSplitPaneWithIDEASplitter(this.mainPanel);
GUIUtil.updateSplitterProportion(this.mainPanel, 0.7F);
//通知显示方式设置修改
EventUtil.subscribe(project, this, DisplayModeSettingsListener.TOPIC, this.displayModeSettingsListener);
}

/**
* 构建界面UI
*/
public void rebuild()
{
//获取文件树展示方式
this.displayMode = GeneralProjectSettings.getInstance(getProject()).
getBrowserSettings().getBrowserDisplayMode();
this.browserPanel.removeAll();
DisposerUtil.dispose(this.browserForm);
//按照显示方式,创建Tree显示窗体
this.browserForm = (this.displayMode == BrowserDisplayMode.SINGLE ?
new SimpleBrowserForm(this) :
this.displayMode == BrowserDisplayMode.TABBED ?
new TabbedBrowserForm(this) : null);
this.browserPanel.add(this.browserForm.getComponent(), "Center");
this.browserPanel.revalidate();
this.browserPanel.repaint();
Disposer.register(this, this.browserForm);
if (objectPropertiesForm != null)
{
objectPropertiesForm.cleanObjectPropertiesShow();
}
}

文件属性对象展示窗体ObjectPropertiesForm

  对于HDFS文件系统对象目录或者文件,显示其所有者、创建日期、文件或者目录大小、文件或者目录的路径、文件或目录备份数 。ObjectPropertiesForm窗体代码在fsobject 包中。到后续讲到FileSystemObject对象的时候再说吧,也比较简单。

文件系统界面上的工具栏Action。

  工具栏功能包括,HDFS连接设置、TreeNode导航(前一节点、后一节点)、TreeNode的折叠和展开、隐藏和显示属性对象UI。所有的工具栏功能按钮都是Action插件对象。代码组织如下:

总结

  本节,从文件系统树的UI展示上做了说明,介绍了主窗体UI、Tab窗体UI、Simple窗体UI、文件系统对象属性ObjectProperties UI等。这部分主要集中在UI界面开发这块。前面说过,IDEA开发UI界面使用 GUI Form的开发方式,UI对象的布局保存在XML中,那么IDEA框架在运行插件的时候,怎么把UI布局的XML转换为Swing对象?这里需要注意一下:GUI布局文件,采用的是IDEA本身的编译器编译的,在IDEA设置中,找到GUI Designer 项,在Generate GUI into 选中 Java source Code ,如下下图,这样编译后,框架会把UI的XML布局自动生成代码插入到源码文件中: