前言
经过重构后,代码已经比较稳定,代码逻辑的联想与查询也变顺畅了,于是继续前进。
多文档编辑器有以下几个好处
- 减少文档间切换的鼠标/键盘操作难度。
- 使注意力仍停留在一个窗口中。
一个完全功能的标签页可以参考浏览器,甚至有手势操作。本篇不打算面面俱到,只谈基本的多文档打开/重复打开/关闭。
用例分析
- 打开文档,自动产生一个标签页,标题为文件名称,提示文本为文件路径字符串。
- 打开重复文档,会自动切换已经存在的标签页,不必产生同样的标签。
- 关闭文档,默认关闭选中的标签及面板。
用例实现
- 打开文档
digraph G{
rankdir=LR;
node[fontname="宋体"];
menu[label="菜单"];
last[label="最近使用的文档"];
drag[label="拖放"];
other[label="其它方式"];
open[label="打开"];
menu->open;
last->open;
drag->open;
other->open;
open->editor->TabbedPaneEditor->ScrollPaneEditor->TextPaneEditor;
}
- 中文表示UI操作;
- 英文表示“处理打开文档逻辑”的关键实例。
图表只是一个设计参考作用,Let's code
首先添加标签
ScrollPaneTextEditor scrollPaneTextEditor = new ScrollPaneTextEditor();
addTab(file.getName(), null, scrollPaneTextEditor, file.getAbsolutePath());
mapComponent.put(hashCode, scrollPaneTextEditor);
scrollPaneTextEditor.textPaneEditor.fileRead(file);
其中
addTab(file.getName(), null, scrollPaneTextEditor, file.getAbsolutePath());
添加了标题/控件/工具提示文本。
图标为null,它属于图标设计领域的问题。以后开始整体设计编辑器的icon时,再补上,现在不关心。
其中的 fileRead(file) 是对旧过程式代码重构,变成对象的成员方法。
il0vec:从零开始写文本编辑器(十四):对已经较稳定的过程式代码进行对象式重构zhuanlan.zhihu.compublic void fileRead(File file) {
if (file != null) {
try {
final int INIT_BUF_SIZE = 10 * 1024;
char[] buffer = new char[INIT_BUF_SIZE];
FileReader fileReader = new FileReader(file);
StringWriter stringWriter = new StringWriter(INIT_BUF_SIZE);
for (;;) {
int count = fileReader.read(buffer);
if (count != -1) {
stringWriter.append(new String(buffer, 0, count));
} else {
break;
}
}
stringInsert(stringWriter);
stringWriter.close();
fileReader.close();
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
} else {
// do nothing.
}
}
这段代码本身没问题,但是写在主线程,所以会巨特么卡,我试了加载300KB的文档,花了2秒。优化的问题肯定是放最后再做的,现在不关心。
- 打开重复文档
在前面有一行
mapComponent.put(hashCode, scrollPaneTextEditor);
其中的 mapComponent 是一个映射表,用来判断标签是否已经存在。Swing的 JTabbedPane 没有直接提供一个 cache,这样很棒,因为它把标签管理做好就完事,不管闲事!但我们要做点额外的映射逻辑。
映射这里直接采用String.hashCode,File 的天然string是绝对路径,所以写一个产生式
private Integer genKey(File file) {
return genKey(file.getAbsolutePath());
}
private Integer genKey(String filePath) {
return filePath.hashCode();
}
散列映射表
private HashMap<Integer, Component> mapComponent;
初始化
public TabbedPaneEditors() {
super();
setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
addMouseListener(TabbedPaneEditors.this);
init();
}
private void init() {
mapComponent = new HashMap<Integer, Component>();
}
使用散列映射表来判断
private void fileOpen(File file) {
int hashCode = genKey(file);
if (mapComponent.containsKey(hashCode)) {
setSelectedComponent(mapComponent.get(hashCode));
} else {
ScrollPaneTextEditor scrollPaneTextEditor = new ScrollPaneTextEditor();
addTab(file.getName(), null, scrollPaneTextEditor, file.getAbsolutePath());
mapComponent.put(hashCode, scrollPaneTextEditor);
scrollPaneTextEditor.textPaneEditor.fileRead(file);
}
}
还没有完,开了头要收尾,在关闭文档中也有一段“回收代码“
- 关闭文档
关闭文档时其实要干很多事情,这里主要是两件事:
- 删除对应的标签
- 删除对应的散列映射记录项
首先删除标签
有多种GUI来让用户关闭,我这里采用弹出菜单的“关闭”菜单项,让用户点击。
public static class TabbedPaneEditors extends JTabbedPane implements MouseListener {
...
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3) {
JPopupMenu popupMenu = new JPopupMenu();
{
JMenuItem menuItem = new JMenuItem(Editor.sInstance.getString("close"));
menuItem.addActionListener(ActionListenerClose);
popupMenu.add(menuItem);
}
popupMenu.show(TabbedPaneEditors.this, e.getX(), e.getY());
}
}
private ActionListener ActionListenerClose = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int index = TabbedPaneEditors.this.getSelectedIndex();
if (index != -1) {
TabbedPaneEditors.this.mapComponent.remove(genKey(getToolTipTextAt(index)));
TabbedPaneEditors.this.removeTabAt(index);
} else {
// do nothing.
}
}
};
}
这里有一个潜在的BUG
TabbedPaneEditors.this.mapComponent.remove(genKey(getToolTipTextAt(index)));
TabbedPaneEditors.this.removeTabAt(index);
这两行代码都用了局部变量 index
如果次序变了,则会产生不可见内存泄露。哈哈哈!因为GUI层的标签正确被删除了。但是映射表的记录删除错了,直到发生索引越界错误 indexOutOfBound。
我还没想到更好办法,因为 index 是一个索引的硬编码,最好是换成对象引用(最多产生空指针异常),否则风险永远在,但代码又会变复杂。
有一个彩蛋:
为什么仅用绝对路径的hashCode,单向散列,怎么反向删除呢?
- 我开始也是觉得要再写一个映射,来产生反向映射。否则不知道key,如何删除记录?
- 我后来又想写一个类来包装value,再中value中解出 key的源字符串,以及 component?
其实啊,还是把问题想复杂了,可以利用 tooltip 这个天然接口存在原始的 file path。这一种天然设计,而不是偶然想到的。Swing的设计者并没有把 tabbedPane 设计成 DefaultMutableTreeNode 那样,在内部用 get-set 提供一个
/** optional user object */
protected transient Object userObject;
因为真的不需要
TabbedPaneEditors.this.mapComponent.remove(genKey(getToolTipTextAt(index)));
需要的只是“读懂源码”。
以上~