JAVA.SWT/JFace: JFace篇之文本处理

  • Post author:
  • Post category:java


《Eclipse SWT/JFACE 核心应用》 清华大学出版社 21 文本处理

JFace提供的文本处理功能非常强大,提醒在Eclipse平台中编辑器的部分。例如代码的折叠、内容提示、代码的格式化等,这些都是基于JFace的文本处理框架的。

21.1 文本处理概述

JFace中有关文本处理的部分分别打包在以下两个包中:

◆ org.eclipse.jface.text_3.6.0.v20100526-0800.jar

◆ org.eclipse.text_3.5.0.v20100601-1300.jar

文本的处理在各种语言的编辑器中经常使用,功能主要有语法着色、语法检查、内容辅助提示等,JFace的文本处理框架的功能远不止这些,功能非常强大。

21.2 项目实战:JavaScript编辑器

该编辑器实现的主要功能如下:

◇ 文件的打开、保存和打印。

◇ 可自定义不同关键字的颜色,如注释的颜色、字符串的颜色、JavaScript关键字的颜色和一些JavaScript内置对象的颜色。

◇ 可自定义显示文本的字体格式。

◇ 对文本进行撤销和重复操作。

◇ 可对文本进行查找和替换功能。

◇ 当输入代码的过程中,可以在提示JavaScript语法中内置对象的内容提示功能。

本项目的文件结果:

◆ 本项目包中的类:

◇ Constants.Java:保存了程序中使用的一些字符串常量。

◇ EventManager.java:集中处理事件的类。

◇ JSCodeScanner.java:搜索代码规则的类。

◇ JSEditor.java:程序的主窗口类。

◇ JSEditorConfiguration.java:编辑器配置类。

◇ JSEditorTestDrive.java:程序的入口类。

◇ JSPreferencePage.java:设置首选项页面。

◇ ObjectContentAssistant.java:内容助手类。

◇ ObjectDetector.java:搜索内置对象字符类。

◇ KeywordDetector.java:搜索关键字对象字符类。

◇ PersistentDocument.java:持久化文档类。

◇ ResourceManager.java:程序中涉及的一些资源的管理。

◆ 本项目action包中的类:

◇ HelpAction.java:帮助菜单项。

◇ OpenAction.java:打开文件菜单项。

◇ PreferenceAction.java:打开首选项对话框项。

◇ PrintAction.java:打印菜单项。

◇ RedoAction.java:撤销菜单项。

◇ SaveAction:保存菜单项。

◇ SearchAction.java:查找替换菜单项。

◇ UndoAction.java:重复此次操作菜单项。

◆ 本项目dialog包中的类:

◇ AboutDialog.java:关于对话框。

◇ FindAndReplace.java:查找替换对话框。

21.3 主窗口模块

主窗口模块是本程序的主界面,继承自ApplicationWindow,这是一个应用程序窗口,窗口中还添加了菜单栏和工具栏。

该主窗口程序中显示代码的控件是SourceViewer对象。代码如下:

package www.swt.com.ch21;

import org.eclipse.jface.action.*;

public class JSEditor extends ApplicationWindow implements IPropertyChangeListener {

private PersistentDocument document;//数据文档对象

private SourceViewer viewer;//显示文本控件对象

private EventManager eventManager;//事件管理器

private PreferenceStore preference;//保存首选项设置的对象

private IUndoManager undoManager;//撤销与重复管理器

private JSEditorConfiguration configuration;//文本控件的配置对象

public JSEditor() {

super(null);

eventManager = new EventManager(this);//初始化事件管理器

this.addMenuBar();//添加菜单

this.addToolBar( SWT.FLAT );//添加工具栏

}

protected void configureShell(Shell shell) {

super.configureShell(shell);

shell.setText(“JavaScript 编辑器”);

shell.setSize(600, 400);

}

protected Control createContents(Composite parent) {

Composite top = new Composite(parent,SWT.NONE);

top.setLayout( new FillLayout());

//初始化文档对象

document = new PersistentDocument();

//初始化源文件显示控件对象

viewer = new SourceViewer(top, new VerticalRuler(10), SWT.V_SCROLL | SWT.H_SCROLL);

//初始化文档的配置对象

configuration = new JSEditorConfiguration(this);

viewer.configure(configuration );//设置文档配置

viewer.setDocument(document);//设置文档

undoManager = new DefaultUndoManager(100);//初始化撤销管理器对象,默认可撤销100次

undoManager.connect(viewer);//将该撤销管理器应用于文档

//初始化代码中的字体

initCodeFont();

return parent;

}

//初始化代码的字体

private void initCodeFont() {

// 定义一个默认的字体

FontData defaultFont = new FontData(“Courier New”, 10, SWT.NORMAL);

// 如果从首选项读出的字体有异常,则使用默认的字体

FontData data = StringConverter.asFontData(ResourceManager

.getPreferenceStore().getString(Constants.CODE_FONT),

defaultFont);

// 创建字体

Font font = new Font(this.getShell().getDisplay(), data);

viewer.getTextWidget().setFont(font);// 设置字体

}

//初始化菜单项

protected MenuManager createMenuManager() {

MenuManager top = new MenuManager();

MenuManager fileMenu = new MenuManager(“文件(&F)”);

MenuManager editMenu = new MenuManager(“编辑(&E)”);

MenuManager helpMenu = new MenuManager(“帮助(&H)”);

fileMenu.add( new OpenAction(this) );

fileMenu.add( new SaveAction(this) );

fileMenu.add( new Separator());

fileMenu.add( new PrintAction(this) );

editMenu.add( new UndoAction(this));

editMenu.add( new RedoAction(this));

editMenu.add( new Separator());

editMenu.add( new SearchAction(this));

editMenu.add( new Separator());

editMenu.add( new PreferenceAction(this));

helpMenu.add( new HelpAction(this));

top.add( fileMenu );

top.add( editMenu ) ;

top.add(helpMenu);

return top;

}

//初始化工具栏

protected ToolBarManager createToolBarManager(int style) {

ToolBarManager toolBar = new ToolBarManager(style);

toolBar.add(new OpenAction(this));

toolBar.add( new SaveAction(this) );

toolBar.add( new Separator());

toolBar.add( new PrintAction(this) );

toolBar.add( new UndoAction(this));

toolBar.add( new RedoAction(this));

toolBar.add( new Separator());

toolBar.add( new SearchAction(this));

toolBar.add( new Separator());

toolBar.add( new PreferenceAction(this));

toolBar.add( new HelpAction(this));

return toolBar;

}

public PersistentDocument getDocument() {

return document;

}

public SourceViewer getViewer() {

return viewer;

}

public EventManager getEventManager() {

return eventManager;

}

public PreferenceStore getPreference() {

return preference;

}

public void setPreference(PreferenceStore preference) {

this.preference = preference;

}

public IUndoManager getUndoManager() {

return undoManager;

}

public JSEditorConfiguration getConfiguration() {

return configuration;

}

//为IPropertyChangeListener接口中的方法,当设置首选项的字体时

//重新设置编辑器的字体

public void propertyChange(PropertyChangeEvent event) {

if (event.getNewValue()==null)

return;

if (event.getProperty().equals(Constants.CODE_FONT))

eventManager.setCodeFont( (FontData[]) event.getNewValue());

}

}

主窗口程序代码分析:

◆ TextViewer对象:

TextViewer对象可以理解为是文本处理的MVC对象,TreeView底层封装了Tree控件,使树的数据和视图分开,而TextViewer底层封装的是StyledText对象,用于显示文本,文本的数据部分则是由实现了IDocument接口的对象提供的。创建一个简单的文本编辑器如下:

TextViewer viewer = new TextViewer(shell, SWT.V_SCROLL|SWT.H_SCROLL);

viewer.setDocument(new Document());

在本程序中使用的是SourceViewer对象,该类继承自TextViewer,除了一般的编辑文本的功能外,增加了编辑源代码的功能,适用作代码编辑器。另外,该类还有一个子类ProjectionViewer类,比SourceViewer功能更加丰富,比如说可以提供代码折叠的效果等。

◆ IDocument接口:

处理文本另一个重要的概念就是IDocument接口,它提供了文本的数据,包括文本的修改、字符的位置、行数等属性和操作。实现该接口常用的有Document类和ProjectionDocument类,后者主要是与ProjectionViewer一起使用,通常情况下使用Document类就可以了。

本程序中使用的PersistentDocument类继承自Document类,为文档增加了打开和保存数据的功能。代码如下:

package www.swt.com.ch21;

import java.io.*;

import org.eclipse.jface.text.*;

public class PersistentDocument extends Document implements IDocumentListener {

private String fileName;//打开的文件名

private boolean dirty;//文件是否修改过

public PersistentDocument() {

this.addDocumentListener(this);//为该文档注册文档监听器

}

//保存到指定的文件

public void save() throws IOException {

if (fileName == null)

throw new IllegalStateException(“文件名不为空!”);

BufferedWriter out = null;

try {

out = new BufferedWriter(new FileWriter(fileName));

out.write(get());

dirty = false;

} finally {

try {

if (out != null)

out.close();

} catch (IOException e) {

}

}

}

//打开文件的方法

public void open() throws IOException {

if (fileName == null)

throw new IllegalStateException(“文件名不为空!”);

BufferedReader in = null;

try {

in = new BufferedReader(new FileReader(fileName));

StringBuffer buf = new StringBuffer();

int n;

while ((n = in.read()) != -1) {

buf.append((char) n);

}

set(buf.toString());

dirty = false;

} finally {

try {

if (in != null)

in.close();

} catch (IOException e) {

}

}

}

public boolean isDirty() {

return dirty;

}

public void setDirty(boolean dirty) {

this.dirty = dirty;

}

public String getFileName() {

return fileName;

}

public void setFileName(String fileName) {

this.fileName = fileName;

}

//接口中的方法,空实现

public void documentAboutToBeChanged(DocumentEvent arg0) {

}

//接口中的方法,当文档修改后,设置已修改状态

public void documentChanged(DocumentEvent arg0) {

dirty = true;

}

}

对文档中的字符的获取是通过父类的get()方法获得的,而设置文档字符的方法是通过父类中的set()方法设置的。

启动主窗口程序:
在启动主窗口程序之前,首先加载首选项文件,因为代码着色部分要使用首选项保存文件中的对各种颜色的设置。
该启动程序的代码实现如下:

package www.swt.com.ch21;

import java.io.IOException;

public class JSEditorTestDrive {

public static void main(String[] args) {

//创建主窗口对象

JSEditor jsApp = new JSEditor();

//为主窗口对象设置首选项对象

//首选项对象通过ResourceManager中的静态方法获得

jsApp.setPreference(ResourceManager.getPreferenceStore() );

//并为首选项保存对象注册选项改变监听器

ResourceManager.getPreferenceStore().addPropertyChangeListener(jsApp);

jsApp.setBlockOnOpen(true);

jsApp.open();

Display.getCurrent().dispose();

//窗口关闭后保存设置的首选项

try {

ResourceManager.getPreferenceStore().save();

} catch (IOException e) {

e.printStackTrace();

}

}

}

21.4 代码着色

对代码着色要遵循一定的规则,比如说要区别注释和关键字,也要区别声明的字符和内置的一些对象等。

◆ 源代码配置类(SourceViewerConfiguration)

要实现对代码的着色,主要是通过设置SourceView对象中的configure方法来实现的:

viewer.configure(configuration );//设置文档配置

其中,configuration是JSEditorConfiguration配置对象,对代码实现的着色和内容辅助的功能都是通过该对象来设置的。JSEditorConfiguration类继承自SourceViewerConfiguration类,通过覆盖父类中的方法可以实现对代码进行着色和内容辅助等功能。代码如下:

package www.swt.com.ch21;

import org.eclipse.jface.text.IDocument;

import org.eclipse.jface.text.contentassist.*;

import org.eclipse.jface.text.presentation.*;

import org.eclipse.jface.text.rules.*;

import org.eclipse.jface.text.source.*;

public class JSEditorConfiguration extends SourceViewerConfiguration {

private JSEditor editor ;

public JSEditorConfiguration( JSEditor editor ){

this.editor=editor;

}

//覆盖父类中的方法,主要提供代码着色功能

public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) {

PresentationReconciler reconciler = new PresentationReconciler();

DefaultDamagerRepairer dr = new DefaultDamagerRepairer(new JSCodeScanner());

reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);

reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);

return reconciler;

}

//覆盖父类中的方法,主要提供内容辅助功能

public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {

//创建内容助手对象

ContentAssistant contentAssistant = new ContentAssistant();

//设置提示的内容

contentAssistant.setContentAssistProcessor(new ObjectContentAssistant(),IDocument.DEFAULT_CONTENT_TYPE);

//设置自动激活提示

contentAssistant.enableAutoActivation(true);

//设置自动激活提示的时间为500毫秒

contentAssistant.setAutoActivationDelay(500);

return contentAssistant;

}

}

覆盖父类中的getPresentationReconciler方法,可以设置当输入文本时可处理的一些事件。IPresentationReconciler接口负责处理文件修改的过程,PresentationReconciler实现了该接口。当文本修改时,可以任务当前的文本不再正确。此时,会调用IPresentationDamager对象计算被修改文档所在的区域,而IPresentationRepairer对象将利用新的文本属性来进行修改,所以要使用setDamager和setRepairer方法。

另外DefaultDamagerRepairer对象实现了IPresentationDamager和IPresentationRepairer接口。该对象可以通过设定一下规则进行代码扫描,当扫描的代码符合规则时,就会按照规则指定代码样式修改。

◆ 基于规则的代码扫描器类(RuleBasedScanner)

具体设置代码修复的规则如下:

DefaultDamagerRepairer dr = new DefaultDamagerRepairer(new JSCodeScanner());

在以上的代码中创建了一个JSCodeScanner对象。该对象即为扫描代码的规则对象。代码如下:

package www.swt.com.ch21;

import java.util.ArrayList;

import java.util.List;

import org.eclipse.jface.text.TextAttribute;

import org.eclipse.jface.text.rules.*;

import org.eclipse.swt.SWT;

public class JSCodeScanner extends RuleBasedScanner {

private TextAttribute keywords ;//关键字的文本属性

private TextAttribute string ;//字符串的文本属性

private TextAttribute object ;//内置对象的文本属性

private TextAttribute comment ;//注释部分的文本属性

public JSCodeScanner(){

//获得首选项中所设置的颜色,初始化各文本属性

keywords = new TextAttribute (ResourceManager.getColor(Constants.COLOR_KEYWORD),null,SWT.BOLD);

string = new TextAttribute (ResourceManager.getColor(Constants.COLOR_STRING));

object = new TextAttribute (ResourceManager.getColor(Constants.COLOR_OBJECT));

comment = new TextAttribute (ResourceManager.getColor(Constants.COLOR_COMMENT),null,SWT.ITALIC);

//设置代码的规则

setupRules();

}

private void setupRules() {

//用一个List集合对象保存所有的规则

List rules = new ArrayList();

//字符串的规则

rules.add(new SingleLineRule(“\”“, “\”“,new Token( string), ‘\’));

rules.add(new SingleLineRule(“’”, “’”, new Token( string), ‘\’));

//注释的规则

rules.add(new SingleLineRule(“/

“, “

/”, new Token( comment), ‘\’));

rules.add(new EndOfLineRule(“//”, new Token( comment),’\’));

//空格的规则

rules.add(new WhitespaceRule(new IWhitespaceDetector() {

public boolean isWhitespace(char c) {

return Character.isWhitespace(c);

}

}));

//关键字的规则

WordRule keywordRule = new WordRule(new KeywordDetector());

for (int i = 0, n = Constants.JS_SYNTAX_KEYWORD.length; i < n; i++)

keywordRule.addWord(Constants.JS_SYNTAX_KEYWORD[i], new Token( keywords ));

rules.add(keywordRule);

//内置对象的规则

WordRule objectRule = new WordRule(new ObjectDetector());

for (int i = 0, n = Constants.JS_SYNTAX_BUILDIB_OBJECT.length; i < n; i++)

objectRule.addWord(Constants.JS_SYNTAX_BUILDIB_OBJECT[i], new Token( object ));

rules.add(objectRule);

//集合类中保存的规则转化为数组

IRule[] result = new IRule[rules.size()];

rules.toArray(result);

//调用父类中的方法,设置规则

//此方法非常重要

setRules(result);

}

}

使用该类方法的主要目的是,为扫描代码提供多个规则。可以这样理解,当修改文本时,程序会自动搜索所有设置的规则,如果当前的文本符合设置的某一个规则时,就按照这个规则所设定的文字样式重新显示文本。所以,对规则的设定直接影响着如何对不同的代码进行着色。
该类JSCodeScanner继承自RuleBasedScanner,这样就可以使用设置的规则来对代码进行扫描了。

◆ 设置代码扫描规则

所有实现了IRule接口的规则如下:

EndOfLineRule, MultiLineRule, NumberRule, PatternRule, SingleLineRule, WhitespaceRule, WordPatternRule, WordRule

该程序中使用的规则类:
● SingleLineRule:单行规则类
    SingleLineRule可以设置指定两个字符间的规则。它的构造方法有:
     ⊙ SingleLineRule(String startSequence, String endSequence, IToken token) :startSequence表示起始的字符,endSequence表示终止的字符,token为符合该规则时所应用的代码样式IToken对象。
     ⊙ SingleLineRule(String startSequence, String endSequence, IToken token, char escapeCharacter) :escapeCharacter表示转义符。
     ⊙ SingleLineRule(String startSequence, String endSequence, IToken token, char escapeCharacter, boolean breaksOnEOF) :breaksOnEOF表示是否在文件末尾终止该规则。
     ⊙ SingleLineRule(String startSequence, String endSequence, IToken token, char escapeCharacter, boolean breaksOnEOF, boolean escapeContinuesLine) :escapeContinuesLine表示是否在该行结束后使用转义符。
 rules.add(new SingleLineRule("/*", "*/", new Token( comment), '\\'));
表示该规则是在两个字符“/*”和“*/”之间应用,也就是本例程序中注释部分的规则。

● EndOfLineRule:没有终止字符的规则类
EndOfLineRule对象可以设置只有初始字符相同后的规则。
 rules.add(new EndOfLineRule("//", new Token( comment),'\\'));
表示带有“//”标记后的都可以应用此规则。

● WordRule:单词规则
WordRule对象应用于对某些特定的字符设置的规则,一般可以用来设置关键字的规则,当输入的字符串为规则中所指定的一些字符串时,就可以使用该对象。
构造方法如下:
     ⊙ WordRule(IWordDetector detector) :其中IWordDetector为接口,创建WordRule对象时要使用实现这个接口的对象。
     ⊙ WordRule(IWordDetector detector, IToken defaultToken) :也可以设置默认的IToken代码格式对象。
 本程序中对关键字设置的规则使用的就是WordRule规则:
 WordRule keywordRule = new WordRule(new KeywordDetector());
其中KeywordDetector对象是实现了IWordDetector的对象,代码如下:

package www.swt.com.ch21;

import org.eclipse.jface.text.rules.IWordDetector;

public class KeywordDetector implements IWordDetector {

// 接口中的方法,字符是否是单词的开始

public boolean isWordStart(char c) {

//循环所有的关键字

//如果有关键字中的第一个字符匹配该字符,则返回true

for (int i = 0, n = Constants.JS_SYNTAX_KEYWORD.length; i < n; i++)

if (c == ((String) Constants.JS_SYNTAX_KEYWORD[i]).charAt(0))

return true;

return false;

}

// 接口中的方法,字符是否是单词中的一部分

public boolean isWordPart(char c) {

//循环所有的关键字

//如果关键字的字符中有该字符,则返回true

for (int i = 0, n = Constants.JS_SYNTAX_KEYWORD.length; i < n; i++)

if (((String) Constants.JS_SYNTAX_KEYWORD[i]).indexOf(c) != -1)

return true;

return false;

}

}

另外,创建WordRule对象后,还要使用以下方法将该规则所应用的字符添加到该对象中:

void addWord(String word, IToken token)

word为该规则应用的字符串,IToken为规则匹配后所应用的代码样式。例如本程序中循环所有关键字字符,然后添加到对象中:

for (int i = 0, n = Constants.JS_SYNTAX_KEYWORD.length; i < n; i++)

keywordRule.addWord(Constants.JS_SYNTAX_KEYWORD[i], new Token( keywords ));

同理,对JavaScript内置对象也是通过此规则来设定的。代码如下:

package www.swt.com.ch21;

import org.eclipse.jface.text.rules.IWordDetector;

public class ObjectDetector implements IWordDetector {

public boolean isWordStart(char c) {

for (int i = 0, n = Constants.JS_SYNTAX_BUILDIB_OBJECT.length; i < n; i++)

if (c == ((String) Constants.JS_SYNTAX_BUILDIB_OBJECT[i]).charAt(0))

return true;

return false;

}

public boolean isWordPart(char c) {

for (int i = 0, n = Constants.JS_SYNTAX_BUILDIB_OBJECT.length; i < n; i++)

if (((String) Constants.JS_SYNTAX_BUILDIB_OBJECT[i]).indexOf(c) != -1)

return true;

return false;

}

}

疑问:这样的匹配方式是否严格?
举例:如果定义了关键字“abstract”、“super”,输入“auper”时开始字符与“abstract”匹配,后面的部分与“super”匹配,则“auper”是否会被认为是关键字?
答案:不会。
原因:WordRule在最后还会进行全词匹配???

◆ 提取类(Token)和文本属性类(TextAttribute)

IToken为一个接口,实现该接口的类是Token,在规则中使用Token对象可以将符合该规则字符转换成对应Token中设置的文本格式。对于文本格式的设置使用的是TextAttribute类。

TextAttribute类的构造方法:
     ⊙ TextAttribute(Color foreground) :foreground为字符显示的前景色。
     ⊙ TextAttribute(Color foreground, Color background, int style) :background为字符显示背景色,style表示样式常量,SWT.BOLD表示加粗,SWT.ITALIC表示倾斜。

最后,本程序中规则的设置的字体的颜色是从首选项获得的,参考ResourceManager类相关的代码部分。