8.2. Editor PartThe code defining the editor's behavior is found in a class implementing the org.eclipse.ui.IEditorPart interface, typically by subclassing either the org.eclipse.ui.part.EditorPart abstract class or org.eclipse.ui.part.MultiPageEditorPart. The Properties editor subclasses MultiPageEditorPart and provides two pages for the user to edit its content. 8.2.1. Editor methodsHere are the EditorPart methods.
MultiPageEditorPart provides the following additional methods:
8.2.2. Editor controlsThe new PropertiesEditor is a multipage editor containing Properties and Source pages. The Properties page contains a tree displaying the property key/value pairs, while the Source page displays the text as it appears in the file itself. These pages showcase building an editor out of individual controls (Properties page) and nesting one type of editor inside another (Source page). Start by creating a new subclass of MultiPageEditorPart. The new PropertiesEditor class contains an init() method ensuring that the appropriate type of content is being edited. package com.qualityeclipse.favorites.editors; import ... import com.qualityeclipse.favorites.FavoritesLog; public class PropertiesEditor extends MultiPageEditorPart { public void init(IEditorSite site, IEditorInput input) throws PartInitException { if (!(input instanceof IFileEditorInput)) throw new PartInitException( "Invalid Input: Must be IFileEditorInput"); super.init(site, input); } Next, add two fields plus methods to create the Source and Properties pages. private TreeViewer treeViewer; private TextEditor textEditor; protected void createPages() { createPropertiesPage(); createSourcePage(); updateTitle(); } void createPropertiesPage() { treeViewer = new TreeViewer( getContainer(), SWT.MULTI | SWT.FULL_SELECTION); int index = addPage(treeViewer.getControl()); setPageText(index, "Properties"); } void createSourcePage() { try { textEditor = new TextEditor(); int index = addPage(textEditor, getEditorInput()); setPageText(index, "Source"); } catch (PartInitException e) { FavoritesLog.logError("Error creating nested text editor", e); } } void updateTitle() { IEditorInput input = getEditorInput(); setPartName(input.getName()); setTitleToolTip(input.getToolTipText()); } When the focus shifts to the editor, the setFocus() method is called; it must then redirect focus to the appropriate editor based on which page is currently selected. public void setFocus() { switch (getActivePage()) { case 0: treeViewer.getTree().setFocus(); break; case 1: textEditor.setFocus(); break; } } When the user directly or indirectly requests that a marker be revealed, ensure that the Source page is active then redirect the request to the text editor. You could do something different when the Properties page is active, but that would require additional editor model infrastructure. public void gotoMarker(IMarker marker) { setActivePage(1); ((IGotoMarker) textEditor.getAdapter(IGotoMarker.class)) .gotoMarker(marker); } Three methods are involved in saving editor content. If the isSaveAsAllowed() method returns false, then the doSaveAs() method is never called. public boolean isSaveAsAllowed() { return true; } public void doSave(IProgressMonitor monitor) { textEditor.doSave(monitor); } public void doSaveAs() { textEditor.doSaveAs(); setInput(textEditor.getEditorInput()); updateTitle(); } This code defines a very simple editor. When the editor is opened, the first page is an empty tree (the content will be added in the next section), while the second page is an embedded text editor (see Figure 8-3). The editor handles all the normal text editing operations on the second page thanks to the embedded text editor, but the first page needs work. Figure 8-3. The Properties editor's Source page.
First, you need to add columns to the tree by adding two new fields plus additional functionality to the createPropertiesPage() method. Later, if you want the display to look more polished, auto-size the columns in the tree similar to the way the Favorites view is auto-sized. (see Section 7.8, Auto-sizing Table Columns, on page 316). private TreeColumn keyColumn; private TreeColumn valueColumn; void createPropertiesPage() { treeViewer = new TreeViewer( getContainer(), SWT.MULTI | SWT.FULL_SELECTION); Tree tree = treeViewer.getTree(); tree.setHeaderVisible(true); keyColumn = new TreeColumn(tree, SWT.NONE); keyColumn.setText("Key"); keyColumn.setWidth(150); valueColumn = new TreeColumn(tree, SWT.NONE); valueColumn.setText("Value"); valueColumn.setWidth(150); int index = addPage(tree); setPageText(index, "Properties"); } When run, the Properties editor now displays two empty columns on the Properties page (see Figure 8-4). Figure 8-4. Properties editor's Properties page.
8.2.3. Editor modelThe next step is to hook up the tree so that content in the text editor appears in the tree. To accomplish this, you need to build a model capable of parsing the text editor's content, and then attach that model, along with a label provider, to the tree. Of course, there is lots of room for improvement in this model, such as splitting out the parsing, refactoring code into a separate class, and enhancing the parser to handle multiline values; however, it will do for the purposes of this demonstration. Start this process by introducing a new PropertyElement superclass for all property model objects. package com.qualityeclipse.favorites.editors; public abstract class PropertyElement { public static final PropertyElement[] NO_CHILDREN = {}; private PropertyElement parent; public PropertyElement(PropertyElement parent) { this.parent = parent; } public PropertyElement getParent() { return parent; } public abstract PropertyElement[] getChildren(); public abstract void removeFromParent(); } A PropertyEntry object represents a key/value pair in the property file. Note that the next three classes are all interdependent and should be added to your project at the same time. package com.qualityeclipse.favorites.editors; public class PropertyEntry extends PropertyElement { String key; String value; public PropertyEntry( PropertyCategory parent, String key, String value ) { super(parent); this.key = key; this.value = value; } public String getKey() { return key; } public String getValue() { return value; } public PropertyElement[] getChildren() { return NO_CHILDREN; } public void setKey(String text) { if (key.equals(text)) return; key = text; ((PropertyCategory) getParent()).keyChanged(this); } public void setValue(String text) { if (value.equals(text)) return; value = text; ((PropertyCategory) getParent()).valueChanged(this); } public void removeFromParent() { ((PropertyCategory) getParent()).removeEntry(this); } } A PropertyCategory represents a group of related property entries with a comment preceding the group indicating the name. The category can extract its name and entries from a reader object. package com.qualityeclipse.favorites.editors; import ... public class PropertyCategory extends PropertyElement { private String name; private List entries; public PropertyCategory( PropertyFile parent, LineNumberReader reader ) throws IOException { super(parent); // Determine the category name from comments. while (true) { reader.mark(1); int ch = reader.read(); if (ch == -1) break; reader.reset(); if (ch != '#') break; String line = reader.readLine(); if (name == null) { line = line.replace('#', ' ').trim(); if (line.length() > 0) name = line; } } if (name == null) name = ""; // Determine the properties in this category. entries = new ArrayList(); while (true) { reader.mark(1); int ch = reader.read(); if (ch == -1) break; reader.reset(); if (ch == '#') break; String line = reader.readLine(); int index = line.indexOf('='); if (index != -1) { String key = line.substring(0, index).trim(); String value = line.substring(index + 1).trim(); entries.add(new PropertyEntry(this, key, value)); } } } public String getName() { return name; } public Collection getEntries() { return entries; } public PropertyElement[] getChildren() { return (PropertyElement[]) entries.toArray( new PropertyElement[entries.size()]); } public void setName(String text) { if (name.equals(text)) return; name = text; ((PropertyFile) getParent()).nameChanged(this); } public void addEntry(PropertyEntry entry) { if (!entries.contains(entry)) { entries.add(entry); ((PropertyFile) getParent()).entryAdded( this, entry); } } public void removeEntry(PropertyEntry entry) { if (entries.remove(entry)) ((PropertyFile) getParent()).entryRemoved( this, entry); } public void removeFromParent() { ((PropertyFile) getParent()).removeCategory(this); } public void keyChanged(PropertyEntry entry) { ((PropertyFile) getParent()).keyChanged(this, entry); } public void valueChanged(PropertyEntry entry) { ((PropertyFile) getParent()).valueChanged(this, entry); } } The PropertyFile object ties it all together. package com.qualityeclipse.favorites.editors; import ... import com.qualityeclipse.favorites.FavoritesLog; public class PropertyFile extends PropertyElement { private PropertyCategory unnamedCategory; private List categories; private List listeners = new ArrayList(); public PropertyFile(String content) { super(null); categories = new ArrayList(); LineNumberReader reader = new LineNumberReader(new StringReader(content)); try { unnamedCategory = new PropertyCategory(this, reader); while (true) { reader.mark(1); int ch = reader.read(); if (ch == -1) break; reader.reset(); categories.add( new PropertyCategory(this, reader)); } } catch (IOException e) { FavoritesLog.logError(e); } } public PropertyElement[] getChildren() { List children = new ArrayList(); children.addAll(unnamedCategory.getEntries()); children.addAll(categories); return (PropertyElement[]) children.toArray( new PropertyElement[children.size()]); } public void addCategory(PropertyCategory category) { if (!categories.contains(category)) { categories.add(category); categoryAdded(category); } } public void removeCategory(PropertyCategory category) { if (categories.remove(category)) categoryRemoved(category); } public void removeFromParent() { // Nothing to do. } void addPropertyFileListener( PropertyFileListener listener) { if (!listeners.contains(listener)) listeners.add(listener); } void removePropertyFileListener( PropertyFileListener listener) { listeners.remove(listener); } void keyChanged(PropertyCategory category,PropertyEntry entry) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .keyChanged(category, entry); } void valueChanged(PropertyCategory category, PropertyEntry entry) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .valueChanged(category, entry); } void nameChanged(PropertyCategory category) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .nameChanged(category); } void entryAdded(PropertyCategory category,PropertyEntry entry) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .entryAdded(category, entry); } void entryRemoved(PropertyCategory category, PropertyEntry entry) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .entryRemoved(category, entry); } void categoryAdded(PropertyCategory category) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .categoryAdded(category); } void categoryRemoved(PropertyCategory category) { Iterator iter = listeners.iterator(); while (iter.hasNext()) ((PropertyFileListener) iter.next()) .categoryRemoved(category); } } The PropertyFileListener interface is used by the ProperyFile to notify registered listeners, such as PropertiesEditor, that changes have occurred in the model. package com.qualityeclipse.favorites.editors; public interface PropertyFileListener { void keyChanged( PropertyCategory category, PropertyEntry entry); void valueChanged( PropertyCategory category, PropertyEntry entry); void nameChanged( PropertyCategory category); void entryAdded( PropertyCategory category, PropertyEntry entry); void entryRemoved( PropertyCategory category, PropertyEntry entry); void categoryAdded( PropertyCategory category); void categoryRemoved( PropertyCategory category); } 8.2.4. Content providerAll these model objects are useless unless they can be properly displayed in the tree. To accomplish this, you need to create a content provider and label provider. The content provider provides the rows appearing in the tree along with parent/child relationships, but not the actual cell content. package com.qualityeclipse.favorites.editors; import ... public class PropertiesEditorContentProvider implements ITreeContentProvider { public void inputChanged( Viewer viewer, Object oldInput, Object newInput ) { } public Object[] getElements(Object element) { return getChildren(element); } public Object[] getChildren(Object element) { if (element instanceof PropertyElement) return ((PropertyElement) element).getChildren(); return null; } public Object getParent(Object element) { if (element instanceof PropertyElement) return ((PropertyElement) element).getParent(); return null; } public boolean hasChildren(Object element) { if (element instanceof PropertyElement) return ((PropertyElement) element).getChildren().length > 0; return false; } public void dispose() { } } 8.2.5. Label providerThe label provider converts the row element object as returned by the content provider into images and text that can be displayed in the table cells. package com.qualityeclipse.favorites.editors; import ... public class PropertiesEditorLabelProvider extends LabelProvider implements ITableLabelProvider { public Image getColumnImage(Object element, int columnIndex) { return null; } public String getColumnText(Object element, int columnIndex) { if (element instanceof PropertyCategory) { PropertyCategory category = (PropertyCategory) element; switch (columnIndex) { case 0 : return category.getName(); case 1 : return ""; } } if (element instanceof PropertyEntry) { PropertyEntry entry = (PropertyEntry) element; switch (columnIndex) { case 0 : return entry.getKey(); case 1 : return entry.getValue(); } } if (element == null) return "<null>"; return element.toString(); } } Finally, you need to add a new initTreeContent() method, called from the createPages() method, to associate the new content and label providers with the tree. This method is followed by another new method to synchronize the text editor's content with the tree's content. The call to asyncExec() ensures that the updateTreeFromTextEditor method is executed in the UI thread (see Section 4.2.5.1, Display, on page 140 for more on the UI thread). The updateTreeFromTextEditor() method indirectly references code in the org.eclipse.jface.text plug-in, so it must be added to the Favorites plug-in's manifest (see Figure 2-10 on page 73). private PropertiesEditorContentProvider treeContentProvider; void initTreeContent() { treeContentProvider = new PropertiesEditorContentProvider(); treeViewer.setContentProvider(treeContentProvider); treeViewer.setLabelProvider(new PropertiesEditorLabelProvider()); // Reset the input from the text editor's content // after the editor initialization has completed. treeViewer.setInput(new PropertyFile("")); treeViewer.getTree().getDisplay().asyncExec(new Runnable() { public void run() { updateTreeFromTextEditor(); } }); treeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); } void updateTreeFromTextEditor() { PropertyFile propertyFile = new PropertyFile( textEditor .getDocumentProvider() .getDocument(textEditor.getEditorInput()) .get()); treeViewer.setInput(propertyFile); } When all this has been accomplished, the Properties editor's Properties page will have some content (see Figure 8-5). Figure 8-5. Properties editor with new content. |