实现文本编辑器的设计与实现_从零开始写文本编辑器(十五):用标签页实现多文档编辑 打开/重复打开/关闭...

caab3eddbe57b7d10b0414389064889f.png

前言

经过重构后,代码已经比较稳定,代码逻辑的联想与查询也变顺畅了,于是继续前进。

多文档编辑器有以下几个好处

  1. 减少文档间切换的鼠标/键盘操作难度。
  2. 使注意力仍停留在一个窗口中。

一个完全功能的标签页可以参考浏览器,甚至有手势操作。本篇不打算面面俱到,只谈基本的多文档打开/重复打开/关闭。

用例分析

  1. 打开文档,自动产生一个标签页,标题为文件名称,提示文本为文件路径字符串。
  2. 打开重复文档,会自动切换已经存在的标签页,不必产生同样的标签。
  3. 关闭文档,默认关闭选中的标签及面板。

用例实现

  • 打开文档

c51c753e7ccff6b69115cec4a3141dbf.png
流程图-打开文档
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;
}
  1. 中文表示UI操作;
  2. 英文表示“处理打开文档逻辑”的关键实例。

图表只是一个设计参考作用,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.com
0d9ea344e9c562cf7f69e9796be64386.png
public 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秒。优化的问题肯定是放最后再做的,现在不关心。

ba95ee410023ba4498031c239b75670d.png

322bc4e584f0ef78f4194b1b2b0c8c47.png
  • 打开重复文档

在前面有一行

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);
	}
}

还没有完,开了头要收尾,在关闭文档中也有一段“回收代码“

  • 关闭文档

关闭文档时其实要干很多事情,这里主要是两件事:

  1. 删除对应的标签
  2. 删除对应的散列映射记录项

首先删除标签

有多种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.
			}
		}
	};
}

7147f2a71b39c317cc42851dbd997b1a.png

这里有一个潜在的BUG

TabbedPaneEditors.this.mapComponent.remove(genKey(getToolTipTextAt(index)));
TabbedPaneEditors.this.removeTabAt(index);

这两行代码都用了局部变量 index

如果次序变了,则会产生不可见内存泄露。哈哈哈!因为GUI层的标签正确被删除了。但是映射表的记录删除错了,直到发生索引越界错误 indexOutOfBound。

我还没想到更好办法,因为 index 是一个索引的硬编码,最好是换成对象引用(最多产生空指针异常),否则风险永远在,但代码又会变复杂。

有一个彩蛋:

为什么仅用绝对路径的hashCode,单向散列,怎么反向删除呢?

  1. 我开始也是觉得要再写一个映射,来产生反向映射。否则不知道key,如何删除记录?
  2. 我后来又想写一个类来包装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)));

需要的只是“读懂源码”。

497a78ec913331ad97c849424cb9c3bd.png
Editor - 副本 (3)被关闭图

以上~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值