- Building the Model
- Tag Content
- Children: Fixed Number
- Children: Collections
- Dynamic Definition
- Generating DOM from existing XSD
- IDE support
- Working with the DOM
- Class Choosers
- Useful Methods of DomElement and DomManager
- PSI Connection
- Tree Structure
- DOM Reflection
- Highlighting Annotations
- Automatic highlighting (BasicDomElementsInspection)
- Required Children
- Mock and Stable Elements
- Models across multiple files
- DOM Stubs
- Building a DOM-based GUI
- Simple Controls
- Editor-based Controls
- Collection Control
- UI Organization
|Please do not comment this page — you are welcome to post your questions and comments to our Open API and Plugin Development Forum. Our developers monitor this forum closely and will try to help you with your specific problems.|
TODO: DOM <=> PSI, Go To Symbol, editor gutter icon->DOM
This article is intended for plugin writers who create custom web server integrations, or some UI for easy XML editing. It describes the Document Object Model (DOM) in IntelliJ IDEA — an easy way to work with DTD or Schema-based XML models.
The following topics will be covered: working with DOM itself (reading/writing tags content, attributes, and subtags) and easy XML editing in the UI by connecting UI to DOM.
It's assumed that the reader is familiar with Java 5, Swing, IntelliJ IDEA's XML PSI (classes XmlTag, XmlFile, XmlTagValue, etc.), IntelliJ IDEA plugin creation basics (application and project components, file editors). For beginners, I would recommend to first look at the general documentation for plugin writers.
So, how to operate with XML from an IntelliJ IDEA plugin? Usually, one has to take XmlFile, get its root tag, and then find a required sub-tag by path. The path consists of tag names, each of them a string. Typing these everywhere is tedious and error-prone. Let's assume you have the following XML:
Let's say you want to read the contents of the second bar element, namely, "239".
It's not correct to create chained calls like
because each call here may return null.
So the code would probably look like this:
Looks awful, doesnt it? But since IntelliJ IDEA 6.0, there's a better way to do the same thing. You just need to extend a special interface — DomElement.
For example, let's create several interfaces:
Next, you should create a DomFileDescription object, pass to its constructor the root tag name and root element interface, and register it with extension point com.intellij.dom.fileDescription.
You can now get the file element from DomManager. To get the "239" value, you only have to write the following code:
I suppose this looks a little bit nicer. You often work with your model in more than one place. Re-creating the model is too inefficient, so we cache it for you, and any subsequent calls to DomManager.getFileElement() will return the same instance. So, it is useful to invoke this method just once, and then keep everywhere only the "root" object you've obtained. In this case you wont need to repeat that scary first line, and the code will look even nicer.
It is also important to note that with this scenario we avoid potential NullPointerException: our DOM guarantees that every method accessing a tags child will return a not-null element, even if the correspondingly-named sub-tag doesnt exist. That may seem strange at a first glance, but it appears to be rather convenient. How does it work? Simple. Given those interfaces, IntelliJ IDEA generates all code for accessing correct sub-tags and creating model elements at runtime. The sub-tag names and element types are taken from method names, return types and method annotations, if any. In most cases annotations can be omitted, as in our example, but this is discussed further in this article anyway.
Now let us explore more thoroughly what the DOM can do, and look at possible ways of representing various XML concepts such as tag content, attributes or sub-tags. Later we will discuss basic methods for working with the model, as well as cover more advanced functionality. Finally, we'll see how to easily create an UI editor for DOM model elements.
In XML PSI, tag content is referred to as tag value, so well do the same for consistency. To read and change a tag value, you have to add two methods (getter and setter) to your interface, like this:
These method names (getValue and setValue) are standard, and they are used for accessing tag values by default. If you want to use custom method names for the same goal, you should annotate these methods with @TagValue, for example:
As you can see, our accessors work with String values. This is natural, since XML represents a text format, and tag content is always text. But sometimes you may want to operate with integers, booleans, enums, or even class names (they, of course, will be represented in IntelliJ IDEA as PsiClass), and more generic Java types (PsiType). In such cases, you just need to change the type in methods to the one you need, and everything will keep working correctly.
If you operate with even more exotic types, you should tell DOM how to deal with them. First, annotate your accessor methods with the @Convert annotation, and specify your own class that should extend the Converter<T> class, in the annotation. Here T is your exotic type, while Converter<T> is a thing that knows how to convert values between String and T. If the value cannot be converted (for example, "foo" is not convertible into Integer), the converter may return null. Please also note that your implementation should have a no-argument constructor.
Let us consider an interesting case when T represents an enum value. Usually, the converter just searches for enum elements with the names specified in XML. But sometimes, for their names, you may need or want to use values that are not valid Java identifiers. For example, CMP version in EJB may be "1.x" or "2.x", but you can't create Java enums with such names. Well, there's also a solution! Let your enum implement NamedEnum interface, and then name your enum elements as you wish (make them compilable of course). Now, just provide the getValue() implementation that will return the right value to match with XML contents, and voilà!
In our example, the code will look as follows:
As I've already mentioned, tag may have a lot of artifacts besides its value: there can be attributes, children, but rather often (e.g., according to DTD or Schema) it should have only the value. Of course such tags also need a DOM element to associate with. And we provide such an element:
So, you can just specify a particular T when using this interface — and everything will work. Methods that work with String are provided for many reasons. For example, your T is PsiClass. It would be useful to highlight invalid values in UI. To get the value to highlight (the string from the XML file) we have the getStringValue() method. The error message will be taken from the converter via getErrorMessage().
Attributes are also rather simple to deal with. You can read their values, set them, and operate with different types. So it's natural to create something like GenericDomValue<T> and then work as usual. "Something like" will be an inheritor, as shown below:
Consider that you want to work with an attribute named some-class having a value of type PsiClass:
That's all! Now you can get/set values, resolve this PsiClass, get its String representation, etc. The name of the attribute will be taken from the method name (see next paragraph). If you name your method in a special way, you can even omit the annotation. For example:
The DomNameStrategy interface specifies how to convert accessor names to XML element names. Or more precisely, not the full accessor names, but rather the names minus any "get", "set" or "is" prefixes. The strategy class is specified in the @NameStrategy annotation in any DOM element interface. Then any descendants and children of this interface will use this strategy. The default strategy is HyphenNameStrategy, where words are delimited by hyphens (see sample above). Another common variant is JavaNameStrategy that capitalizes the first letter of each word, as in Java's naming convention. In our example, the attribute name would be "someClass".
If attribute doesn't define a PsiClass, but some other custom T that needs a converter, you just need to specify the @Convert annotation to the getter.
Please note that the attributes getter method will never return null, even if the attribute isn't specified in XML. Its getValue(), getStringValue() and getXmlAttribute() methods will return null, but the DOM interface instance will exist and be valid. If the element has an underlying attribute, this can be easily fixed (surely, only if you need that): just call the undefine() method (defined in DomElement), and the XML attribute disappears, while GenericAttributeValue remains valid.
You may often deal with tags that have at most one sub-tag with the given name (e.g. <ejb-name>, <ejb-class> or <cmp-field>) in tags defining entity EJBs. To work with such children, provide getters for them. These getters should have a return type that extends DomElement:
There's also an annotation to designate such children explicitly: @SubTag. Its "value" attribute contains a tag name. If it is not specified, the name is implied from the method name using the current name strategy.
Sometimes it is the subtag's presence that means something, rather than its content — "unchecked" in EJB method permissions, for example. If it exists, then permissions are unchecked, otherwise checked. For such things one should create a special GenericDomValue<Boolean> child. Usually its getValue() returns true if there's "true" in a tag value, false if there's "false" in a tag value, and null otherwise. In the @SubTag annotation, you can specify the attribute like indicator=true. In this case, getValue() will return true if the tag exists and false otherwise.
Let's consider another interesting example inspired by EJB, where a relation that has two roles, each designating one relation end: first role and second role. Both are represented by tags with the same values. So, we could create a collection of role elements, and every time we access some role we would check if this collection has sufficient number of elements. But one of the main purposes of the DOM is to eliminate unnecessary checks. So why cant we have a fixed (more than one) number of children with the same tag name? Let's have them!
The first method will return the DOM element for the first subtag named <ejb-relationship-role>, and the second — for the second one. Hence the term "fixed-number" for such children. According to DTD or Schema, there should be fixed number of subtags with the given name. Most often this fixed number is 1; in our case with the relations it is 2. Just like attributes, fixed-number children exist regardless of underlying tag existence. If you need to delete tags, it can be done with the help of the same undefine() method.
For children of GenericDomValue type, you can also specify a converter, just as you can for attributes.
One more common case in DTD and Schema's is when children have the same tag name and a non-fixed upper limit in count. Their accessors differ from those of the fixed-number children in the following: the return result is Collection or List of a special type that extends DomElement, and if you want to use name strategies, name the method in plural. For example, in EJB we would have the following method:
There's also an annotation @SubTagList where you can explicitly specify the tag name.
Returned collections cannot be modified directly. To delete an element from collection, just call undefine() on this element. The tag will then be removed, and element will become invalid (DomElement.isValid() == false). Note that this behavior differs from that of fixed-number children and attributes: they are always valid, even after undefine(). Again, unlike those children types, collection children always have valid underlying XML tags.
Adding elements is a little bit harder. Since all DOM elements are created internally, you cant just pass some of your DOM elements to some method, to add the element to the collection. In fact, you have to ask a parent element to add a child to the collection. In our example it's done in the following way:
which adds an element to wherever you want, or
which adds a new DOM element to the end of the collection. Please note the singular tense of the word "Entity". That's because here we deal with one Entity object, while in the collection getter we dealt with potentially many entities.
Now, you can do anything you want with the returned value: modify, define the tag's value, children, etc.
The last common case is also a collection, but one consisting of tags with different names that are arbitrarily mixed. To work with it, you should define collection getters for all tag names within the mixed collection, and then define an additional specially annotated getter:
The annotation here is mandatory — IntelliJ IDEA cannot guess several tag names from one method name.
To add elements to such mixed collections, you should create "add" methods for each possible tag name:
The index parameter in the last example means the index in the merged collection, not in the collection of tags named "bar".
You can extend existing DOM model at runtime by implementing com.intellij.util.xml.reflect.DomExtender<T>. Register it in "extenderClass" attribute of EP com.intellij.dom.extender, where "domClass" specifies DOM class <T> to be extended. DomExtensionsRegistrar provides various methods to register dynamic attributes and children.
If the contributed elements depend on anything other than plain XML file content (used framework version, libraries in classpath, ...), make sure to return false from DomExtender#supportsStubs.
DOM can be generated automatically from existing XSD/DTD. Output correctness/completeness will largely depend on the input scheme and may require additional manual adjustments.
Follow these steps (12.1 or later):
- Run IntelliJ IDEA with "Plugin DevKit" enabled and add JVM option -Didea.is.internal=true
- Select Tools | Internal Actions | DevKit | Generate Dom model
- Select Scheme file and set options, then click "Generate" to generate sources
- Modify generated sources according to your needs
|IntelliJ IDEA 13|
The following features are only available in IntelliJ IDEA 13
- DomElement: provide implicit usages for all DOM-related methods defined in inheriting classes (to suppress "unused method" warning)
- DomElementVisitor: provide implicit usages for all DOM-related visitor methods defined in inheriting classes (to suppress "unused method" warning)
It often happens that a collection contains same-named tags that may have different structure or even be represented by different types in the DTD or Schema. As an example, JSF Managed Beans may be of three types. If a <managed-bean> tag contains a <map-entries> sub-tag, then the Managed Bean type is MapEntriesBean. If it contains a <list-entries> sub-tag — can you guess? Right — ListEntriesBean! Otherwise it's a PropertyBean (all three interfaces extend ManagedBean). And when we write List<ManagedBean> getManagedBeans(), we expect to get not only a list where all elements are instances of the ManagedBean interface, but a list where each element is of a certain type, i.e. MapEntriesBean, ListEntriesBean, or PropertyBean.
In such cases one should decide which interface the DOM element should actually implement (relative to the given tag). This is achieved by extending the TypeChooser abstract class:
Here, the first method (chooseType()) does exactly what it is named after (chooses the particular type, most often it's a class). The second one (distinguishTag()) acts in reverse: it modifies a tag so that when the element is read from an XML file next time (for example, after the user has closed and opened the project again), the newly created DOM element will implement the same interface and no model data will be lost. And, finally, getChooserTypes() just returns all the types that could be returned by chooseType().
To make your TypeChooser work, register it in your overridden DomFileDescription.initializeFileDescription() method by calling registerTypeChooser().
Of course, DOM is tightly connected to XML PSI, so there's always a way of getting the XmlTag instance (which can be null for fixed-number children and attributes) using the getXmlTag() method. We remember that in GenericAttributeValue there's also the getXmlAttribute() method. In general case there is getXmlElement() method. You can also get a DOM element by XML element using the DomManager.getDomElement() method.
If DOM element has no underlying XML element, it can be created by calling ensureTagExists(). To delete a tag, use the already known undefine() method. This method will always delete the underlying XML element (tag or attribute). If the element was a collection child, then neither it nor its entire sub-tree will be valid anymore.
In every normal tree there's always a possibility to walk up. DomElement is no exception. Method getParent() just returns element's parent in tree.
The method <T extends DomElement> T getParentOfType(Class<T> requiredClass, boolean strict) returns the tree ancestor of the given class. You can see the standard strict parameter, that can return the DOM element itself, if it's false and your current DOM element is an instance of requiredClass.
Finally, getRoot() will return the DomFileElement, which is the root of every DOM tree.
An element becomes invalid if it has been deleted explicitly or due to external PSI changes. Fixed-number children and attributes are meant to stay valid as long as possible, no matter what happens with XML. They can become invalid only if they have collection tree ancestor that has been deleted.
Newly created DOM elements are always correct and valid, so their isValid() methods will return true.
Element validity is very important, since you cannot invoke any methods on invalid elements (except, of course, isValid() itself).
DOM also has a kind of reflection, called "Generic Info". One would use it to be able to access children by tag names directly, instead of calling getter methods. See GenericInfo interface and getGenericInfo() methods in DomElement and DomManager for more information. There's also DomElement.getXmlElementName() method that returns the name of a corresponding tag or attribute.
TODO: using @Presentation
DomElement.getPresentation() returns an instance of ElementPresentation, an interface that knows presentable element type, name, and sometimes even its icon. Presentations are actually obtained from presentation factory objects that, like ClassChoosers's, should be registered in ElementPresentationManager as early as possible. You can specify type name and icon for all elements of some class, ways of getting type name, icon and presentable name for particular objects. When not specified, presentable name is taken from the object itself, if it contains a method annotated with @NameValue annotation, that returns String or GenericValue. If there's no such method, it will return null. For DomElement, there's another way to get this presentable name: DomElement.getGenericInfo().getElementName().
If you want to be notified on every change in the DOM model, add DomEventListener to DomManager. DOM supports the following events: tag value changed, element defined/undefined/changed, and collection child added/removed.
The DOM supports error checking and highlighting. It's based on annotations which you add to the DOM element in a special place (don't confuse these annotations with the ones of Java 5 — they are very different). You need to implement the DomElementAnnotator interface, and override DomFileDescription.createAnnotator() method, and create this annotator there. In DomElementsAnnotator.annotate(DomElement element, DomElementsProblemsHolder annotator) you should report about all errors and warnings in the element's sub-tree to the annotator (DomElementsProblemsHolder.createProblem()). You should return this annotator in the corresponding virtual method of the DomFileDescription.
The following errors can be highlighted automatically by providing an instance of BasicDomElementsInspection:
- @Required element missing or having empty text
- XML value cannot be converted by some Converter
- name is not unique while it should be
The latter case requires you to specify the name getter with @NameValue annotation (see Presentation). The checking uses the DomFileDescription.getIdentityScope() method to get the element, within which the name should be unique.
To suppress spellchecking annotate your DomElement with @com.intellij.spellchecker.xml.NoSpellchecking.
There is a common case in error highlighting, when one needs to say, that some required sub-tag or attribute is missing. DOM will do this for you automatically, if you annotate the getter for that child with the @Required annotation. For collection children getters, this annotation will mean, that the collection should be not empty (corresponding to '+' sign in DTD). Also, when you create a new element that has required fixed-number or attribute children, their tags or attributes will also be created in XML.
Remember the interface GenericDomValue<T> and its sub-interface GenericAttributeValue<T>? Remember, that ANY class may be passed as T — for example, let's interpret GenericDomValue<PsiClass> as a reference to a class. Then we can always consider it as a reference to an object of class T! With Strings or enums, it is not a very useful idea, but we'll use it in another way. Very often XML has such a structure that an object is declared at some place, and is referenced at some other place (more precisely, in a tag or attribute value). So, if you want to create a method like GenericValue<MyDomElement> getMyDomElementReference(), then you just have to specify a proper converter that will find an instance in your model of MyDomElement with the name specified in the GenericDomValue.
That's the core idea. Since creating such converters is quite boring, we've done it for you. You don't have to annotate reference getters at all, as the name resolution will be made automatically. Elements will be searched by name, and the name will be taken from the method annotated with @NameValue (just like in Presentation). The converter used is DomResolveConverter. It constructor takes a parameter, so it cant be referenced in @Convert annotation, but its subclasses (if you create them) — can. If you still want to specify explicitly that your reference to DomElement should be resolved "model-wide", use the @Resolve annotation parameterized with the desired class. The resolution scope will be taken from the DomFileDescription.getResolveScope().
In addition to the above, auto-resolving in DOM also provides some features in your XML text editor: error highlighting, completion, Find Usages, Rename Refactoring... Unresolved references will be highlighted, and even completed. If you want to create a custom converter and want to have this code insight with it, you should extend not only the Converter but ResolvingConverter. It has one more method getVariants(), where you'll have to provide the collection consisting of all targets your reference may resolve to. Those familiar with PsiReference will recognize this method. The code completion feature will filter these items by prefix, to display proper completion menu.
If you need to choose a Converter depending on other values (e.g. in sibling/parent element) or any runtime condition (e.g. presence or version of library), you can use WrappingConverter. See also GenericDomValueConvertersRegistry for managing an extensible registry of available Converters to choose from.
Your DOM elements do not have to be tied to a disk file. DomManager.createMockElement() will help you to create a non-tied element of a given class with the given module. An element may be physical or not. 'Physical' here means that IntelliJ IDEA will create a mock document for it, so you can enjoy Undo functionality if you pass this document to the right place in IntelliJ IDEA's file editor.
DomElement.copyFrom() allows you to copy information from one DomElement to another. In fact, it just replaces XML tags, and all the old data is lost. Nevertheless, the element's fixed-number children don't become invalid, they only contain new tag values, attribute values, etc. The tree is actually rather conservative.
The combination of createMockElement() and copyFrom() is useful for editing element contents in dialogs. You create a mock copy of an element, work with it in the dialog and then, if the user doesn't cancel, copy the element back to the main model. Since it's a common case, a special shortcut method has been created in DomElement, called createMockCopy().
IntelliJ IDEA's XML parser is incremental: changes in text don't cause the whole file to be reparsed. But you should keep in mind that this rule may sometimes not work correctly. For example, your DOM elements can unexpectedly become broken as a result of manual editing of the XML file (even if it didn't happen inside those elements). If a file editor depends on such a broken element, this can lead to closing the tab, which isn't very nice from the user's point of view. For example, suppose you have an entity bean named "SomeEntity". You open an editor for it, then you go into the XML, change the tag name from entity to session, and then back to entity. Of course, no DOM element can survive after such blasphemy. But notwithstanding, you still want your editor to stay open! Well, there is a solution, and it's called DomManager.createStableValue(Factory factory).
This method creates a DOM element that delegates all its functionality to some real element (returned from the factory parameter). As soon as that real element becomes invalid, the factory is called once more, and if it returns something valid, it becomes the new delegate. And so on... In the example with EJB, the factory would once again look for an Entity Bean named "SomeEntity".
Stable DOM elements also implement the StableElement interface, which has the following methods:
- DomElement getWrappedElement() — just returns the current element to which all method calls are delegated;
- void invalidate() — makes the wrapped element invalid. Any next method call will cause the factory to create a new delegate;
- void revalidate() — calls the factory, and if it returns something new (i.e. not the same as the current wrapped element) invalidates the old value and adopts the new one.
Visitor is a very common design pattern. DOM model also has a visitor, and it's called DomElementVisitor. The DomElement interface has methods accept() and acceptChildren() that take this visitor as a parameter. If you look at the interface DomElementVisitor itself, you may be surprised, since it has only one method: visitDomElement(DomElement). Where is the Visitor pattern? Where are all those methods with names like visitT(T) that are usually found in it? There are no such methods, because the actual interfaces (T's) arent known to anyone except you. But when you instantiate the DomElementVisitor interface, you may add there those visitT() methods, and they will be called! You may even name them just visit(), specify the type of the parameter, and everything will be fine. For example, if you have two DOM element classes — Foo and Bar — your visitor may look like this:
Sometimes you may want to extend your model with some functionality that isn't directly connected with XML, but relates to your program logic. And the most appropriate place for this functionality is the DOM element interface. What to do then?
The simplest case is when you want to add to your interface a method that returns exactly what some other getter in this element (or in one of its children) returns. You can easily write this helper method and annotate it with the @PropertyAccessor annotation, in which you should specify the path consisting of property names (getter names without the "get" or "is" prefixes). For example, you can write:
In this case, the second method will return just the same as the first one. If there were "foo.bar.name" instead of "very-long-name" in the annotation, the system would actually call getFoo().getBar().getName() and return the result to you. Such annotations are useful when you're extending some interface that is inconsistent with your model, or you try to extract a common super-interface from two model interfaces with differently named children that have the same sense (see <ejb-ref> and <ejb-local-ref>}}).
The case just described is simple, but rare. More often, you really have to incorporate some logic into your model. Then nothing except Java code helps you. And it will. Add the desired methods to your interface, then create an abstract class implementing the interface, and implement there only methods that you added manually and that are not directly connected to your XML model. Note that the class should have a constructor with no arguments.
Now you only have to let DOM know that you wish to use this implementation every time you're creating a model element that should implement the necessary interface. Simply register it using
extension point com.intellij.dom.implementation and IntelliJ IDEA will generate at run-time the class that not only implements the needed interface, but also extends your abstract class.
Many frameworks require a set of XML configuration files ("fileset") to work as one model, so resolving/navigation works across all related DOM files.
Depending on implementation/plugin, providing filesets implicitly (using existing framework's setup in project) or via user configuration (usually via dedicated Facet) can be achieved.
Extend DomModelFactory (or BaseDomModelFactory for non-Module scope) and provide implementation of your DomModel. Usually you will want to add searcher/utility methods to work with your DomModel implementation.
Example can be found in Struts 2 plugin (package com.intellij.struts2.dom.struts.model).
Please use it sparingly and only for heavily accessed parts in your DOM model, as it increases disk space usage/indexing run time
DOM elements can be stubbed, so (costly) access to XML/PSI is not necessary (see Indexing and PSI Stubs in IntelliJ IDEA for similar feature for custom languages). Performance relevant elements, tag or attribute getters can simply be annotated with com.intellij.util.xml.Stubbed.
Return true from DomFileDescription#hasStubs and increase DomFileDescription#getStubVersion whenever you change @Stubbed annotations usage in your DOM hierarchy to trigger re-indexing.
All IntelliJ IDEA's forms that deal with DOM are organized in a special way. They support two main things: getting data from XML into the UI, and saving UI data to XML. The former is called resetting, the latter — committing. There's Committable interface that has corresponding methods: commit() and reset(). There's also a way of structuring your forms into smaller parts, namely the Composite pattern: CompositeCommittable. Methods commit() and reset() are invoked automatically by IntelliJ IDEA on editor tab switch or undo. So you only need to ensure that all your Swing structure is organized in a tree of CompositeCommittable, and all the hard work will be done by IntelliJ IDEA.
DOM controls are special descendants of Committable. All of them implement DomUIControl. Note that they are not Swing components — they are only a way of connecting DOM model and Swing components. One end of the connection — the DOM element — is usually specified in the controls constructor. The other end — Swing component — can be obtained in 2 ways. The first is to ask DOM control to create it. But that is rather inconvenient if you want to create the forms in, say, IntelliJ IDEA's GUI Designer. In that case, you'll need the second way: ask the control to bind() to an existing Swing component of a correct type (that depends on the type of value that you're editing). After that, your Swing components will be synchronized with DOM, they'll even highlight errors reported by DomElementsAnnotator.
Sometimes you may need to do some work (enable or disable some components, change their values) after a particular DOM control is committed. Then you should define the addCommitListener() method of that DOM control and override the CommitListener.afterCommit() method. This method will be invoked inside the same write action as the main commit(), so any changes you do in this method to the XML will be merged with the commit() in the Undo queue.
With simple controls, you can edit GenericDomValue: simple text, class names, enums and boolean values. These controls take a special object as a constructor parameter. This object should implement the DomWrapper interface that knows how to set/get values to/from DOM model. This abstraction is sometimes very useful.
We have three major DomWrapper's: DomFixedWrapper<T> redirecting calls to GenericDomValue<T>, DomStringWrapper redirecting calls to string accessors of GenericDomValue, and DomCollectionWrapper that gets/sets values of the first element of the given GenericDomValue collection. Some controls (those having a text field as part of itself) take additional boolean constructor parameter — commitOnEveryChange, whose meaning is evident from the name. We don't recommend using it anywhere except small dialogs, because committing on every change slows down the system significantly.
Most often these controls are created by DomUIFactory.createControl(GenericDomValue). This method understands which control to create by using DOM reflection (GenericInfo, as you probably remember). But sometimes you may want to create the controls directly. So let's look at the simple controls more closely.
It allows you to edit boolean values. The control is bound to JCheckBox.
The control is bound to a non-editable JComboBox, so it can be used to choose something from a limited set. One case of such a limited set is enum. Or it can be a constructor where you can provide a Factory<List<String>>, and return from this factory anything you want (for example, a list of databases IntelliJ IDEA is aware of). By default, the wrong values (written in XML, but not present in the list you've given to the control) are displayed in red. Since it's common practice to specify custom CellRenderer for combo boxes, the control has the isValidValue(String) method. If it returns false on the value you're rendering, you can highlight it in some way, to achieve the same result as the default renderer. Or you can just delegate to that renderer in your own way.
Sometimes, when there are only 2 alternatives, it's convenient to use a check box instead of combo box. This control is designed specially for such cases. While being (and being bound to) a check box, the control edits not just "true" or "false", but any two String values, or two enum elements. In the last case, it has a boolean invertedOrder parameter, to specify which element corresponds to the checked state. By default invertedOrder is set to false, so the first element corresponds to the unchecked state, and the second — to the checked one. If you set the parameter to true, the states will swap.
Please note that editor-based controls are built on IntelliJ IDEA's Editor instead of standard JTextField. Since there's currently no way to instantiate Editor directly through the Open API, controls are bound to special JPanel inheritors, and their bind() method adds the necessary content to those panels.
This control allows you to edit simple string values. The control is bound to the TextPanel component. There's also an inheritor of that panel — MultiLineTextPanel. If you bind a StringControl to it, a big editor will appear on the screen. In case you don't have space for a big editor, bind it to a BigTextPanel. Then it will be filled with a text editor, and the browse button will be added to open a dialog with the big editor where you can type a longer string.
This is a one-line editor with a browse button that opens the standard IntelliJ IDEA class selection dialog. The control accepts class names only. It is bound to PsiClassPanel.
This is almost the same as PsiClassControl, but allows entering not only class names, but also Java primitive types and even arrays. It is bound to PsiTypePanel.
There is a special table component where each row represents one collection child. It's called DomCollectionControl<T>, where T is your collection element type. To function properly, it needs DomElement (parent of the collection), some description of the collection (sub-tag name or a DomCollectionChildDescription from DOM reflection), and a ColumnInfo array. This can be passed to the constructor, or can be created in a DomCollectionControl inheritor, in an overriden method createColumnInfos().
What is a column info? It's just a somewhat more comfortable way to work with the table model. It uses Java 5 generics and is more object-oriented. So, it's named ColumnInfo<Item,Aspect>, where Item is a type variable corresponding to the type of elements in the collection, and Aspect is a type variable corresponding to this particular column information type: String, PsiClass, Boolean, etc. The basic things that a column knows are: column name, column class, reading value (Aspect valueOf(Item)), writing value (setValue(Item item, Aspect aspect)), cell renderer (getRenderer(Item)), cell "editability" (isCellEditable(Item)), cell editor (getEditor(Item)), etc.
There are a lot of predefined column infos, so you'll probably never create a new one. But it's useful to know whos who there.
First, if a collection child is a GenericDomValue, it's usually convenient to edit it directly in the table. For this, you may need one of the following classes: StringColumnInfo, BooleanColumnInfo, or more generic GenericValueColumnInfo. But such collections are encountered very rarely.
A more common case is when a collection element is more complex and has several GenericDomValue children. Then one may create a column for each of those children. The appropriate column info is ChildGenericValueColumnInfo<T>. It will ask you for a DomFixedChildDescription (one more thing from DOM reflection), a renderer and an editor — nothing else. So, the main things left to customize are the renderer and the editor.
As for the renderer, there are two main choices: DefaultTableCellRenderer, and IntelliJ IDEA's BooleanTableCellRenderer. Editors are more complicated, but they closely resemble simple DOM controls.
BooleanTableCellEditor, DefaultCellEditor(JTextField), ComboTableCellEditor, etc. DomUIFactory.createCellEditor() will create any of them automatically (including the editor for PsiClass), so that you won't need to think about which one to select every time.
Collection control is a complex control, so it's bound to a complex Swing component. It's called DomTableView. It has a toolbar (you can override DomTableView.getToolbarPosition() to customize it's location), with Add and Delete buttons. If you want, you may specify custom addition actions in DomCollectionControl.createAdditionActions() (its recommended to extend ControlAddAction). If there is only one addition action, it will be invoked after pressing the Add button; if there are many, then a popup menu will be displayed. To change the removal policy, override the DomCollectionControl.doRemove(List<T>) method.
The toolbar may also have an Edit button, if you specify that DomCollectionControl.isEditable(). To add a behavior to this button, override DomCollectionControl.doEdit(T). There can also be a Help button, if you pass a non-null String helpId parameter while constructing your DomTableView.
If there are no items in the collection, DomTableView may display a special text (DomTableView.getEmptyPaneText()), instead of an empty table.
You can add your own popup menu to the control. Call the DomTableView.installPopup() method after construction, and pass there a DefaultActionGroup with your popup actions.
Table can have single or multiple (default) row selection. If you want to change this behavior, override DomTableView.allowMultipleRowsSelection().
The easiest way to create a DOM-based UI form is to extend the BasicDomElementComponent class. This will require you to pass some DOM element to the constructor. Then you bind an IntelliJ IDEA GUI Designer form to your subclass and design a beautiful form there. You will surely want to bind some controls to DOM UI, in which case you should of course ensure that they have right types. Finally, you should create some DOM controls in class' constructor and bind them. But you can create controls and bind them to the DomElement 's children — GenericDomValue 's automatically.
Just name your components properly and call the bindProperties() method in the constructor. The field names should correspond to the getter names for the element's children. They may also be prefixed with "my". Imagine that you have such DOM interface:
In this case, the UI form class can look like this:
All the fields here are actually bound to controls in a GUI form.
Very often you'll have to create your own file editor. Then, to use all the binding and undo functionality, it's suggested to inherit your FileEditorProvider from PerspectiveFileEditorProvider, create an instance of DomFileEditor there, and pass a BasicDomElementComponent. To easily create an editor with a caption at the top, like in our EJB and JSF, you may use the static method DomFileEditor.createDomFileEditor(). DomFileEditor automatically listens to all changes in the document corresponding to the given DOM element, and therefore refreshes your component on undo. If you want to listen to changes in additional documents, use the methods addWatchedDocument(), removeWatchedDocument(), addWatchedElement(), removeWatchedElement() in DomFileEditor.
Thank you for your time and attention. I hope you've found this article really useful. You are welcome to post your questions and comments to our Open API and Plugin Development Forum.
The following bundled open source plugins make (heavy) use of DOM:
Peter Gromov, JetBrains IntelliJ IDEA core team.