本节,将详细说明一下文件系统树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 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 ); 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 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 ) { 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技术也是同样使用这种方法。
当用户选择以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 中具体实现。
该类比较简单,在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 public SimpleBrowserForm (DisposableProjectComponent parentComponent) { this (parentComponent, new SimpleBrowserTreeModel(parentComponent.getProject(), ConnectionManager.getInstance(parentComponent.getProject()).getConnectionBundle())); }public SimpleBrowserForm (DisposableProjectComponent parentComponent, ConnectionHandler connectionHandler) { this (parentComponent, new TabbedBrowserTreeModel(connectionHandler)); }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 public FileSystemBrowserTree getBrowserTree () { return this .browserTree; }public void selectElement (FileSystemBrowserTreeNode treeNode, boolean focus, boolean scroll) { this .browserTree.selectElement(treeNode, focus); }public void rebuildTree () { this .browserTree.getModel().getRoot().rebuildTreeChildren(); }
该类,将用户创建的多个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 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); }private void initTabs () { Project project = getProject(); TabbedPane oldConnectionTabs = this .connectionTabs; this .connectionTabs = new TabbedPane(this ); ConnectionManager connectionManager = ConnectionManager.getInstance(project); ConnectionBundle connectionBundle = connectionManager.getConnectionBundle(); for (ConnectionHandler connectionHandler : connectionBundle.getConnectionHandlers()) { 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); } 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 @Nullable public FileSystemBrowserTree getBrowserTree () { return getActiveBrowserTree(); }@Nullable public FileSystemBrowserTree getBrowserTree (ConnectionHandler connectionHandler) { SimpleBrowserForm browserForm = getBrowserForm(connectionHandler); return browserForm == null ? null : browserForm.getBrowserTree(); }@Nullable public FileSystemBrowserTree getActiveBrowserTree () { TabInfo tabInfo = this .connectionTabs.getSelectedInfo(); if (tabInfo != null ) { SimpleBrowserForm browserForm = (SimpleBrowserForm) tabInfo.getObject(); return browserForm.getBrowserTree(); } return null ; }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(); } }
该类,将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 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(); } } }; rebuild(); ActionToolbar actionToolbar = ActionUtil.createActionToolbar("" , true , "HadoopNavigator.ActionGroup.Browser.Controls" ); actionToolbar.setTargetComponent(this .actionsPanel); this .actionsPanel.add(actionToolbar.getComponent()); 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); }public void rebuild () { this .displayMode = GeneralProjectSettings.getInstance(getProject()). getBrowserSettings().getBrowserDisplayMode(); this .browserPanel.removeAll(); DisposerUtil.dispose(this .browserForm); 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(); } }
对于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布局自动生成代码插入到源码文件中: