mirror of
https://github.com/JetBrains/intellij-sdk-code-samples.git
synced 2025-07-30 18:27:49 +08:00
xml_dom_api.md: cleanups
This commit is contained in:
parent
66a6a2c37e
commit
5f6869d16a
@ -7,14 +7,15 @@
|
||||
## Abstract
|
||||
|
||||
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 Platform --- an easy way to work with DTD or Schema-based XML models.
|
||||
It describes the *Document Object Model* (DOM) in IntelliJ Platform - 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, Swing, IntelliJ Platform XML PSI (classes [`XmlTag`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlTag.java), [`XmlFile`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlFile.java), [`XmlTagValue`](upsource:///xml/xml-psi-api/src/com/intellij/psi/xml/XmlTagValue.java), etc.), IntelliJ Platform plugin development basics (application and project components, file editors).
|
||||
|
||||
## Introduction
|
||||
|
||||
So, how to operate with XML from an IntelliJ Platform plugin? Usually, one has to take `XmlFile`, get its root tag, and then find a required sub-tag by path.
|
||||
So, how to operate with XML from an IntelliJ Platform 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:
|
||||
@ -33,8 +34,12 @@ 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
|
||||
|
||||
```java
|
||||
file.getDocument().getRootTag().findFirstSubTag("foo").
|
||||
findSubTags("bar")[1].getValue().getTrimmedText()
|
||||
file.getDocument()
|
||||
.getRootTag()
|
||||
.findFirstSubTag("foo")
|
||||
.findSubTags("bar")[1]
|
||||
.getValue()
|
||||
.getTrimmedText();
|
||||
```
|
||||
|
||||
because each call here may return `null`.
|
||||
@ -59,7 +64,8 @@ if (document != null) {
|
||||
}
|
||||
```
|
||||
|
||||
Looks awful, doesn't it? But there's a better way to do the same thing.
|
||||
Looks awful, doesn't it?
|
||||
But there's a better way to do the same thing.
|
||||
You just need to extend a special interface - [`DomElement`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/DomElement.java).
|
||||
|
||||
For example, let's create several interfaces:
|
||||
@ -98,22 +104,23 @@ if (bars.size() > 1) {
|
||||
}
|
||||
```
|
||||
|
||||
I suppose this looks a little bit nicer.
|
||||
I suppose this looks a little 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 won't need to repeat that scary first line, and the code will look even nicer.
|
||||
In this case, you won't 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 doesn't exist.
|
||||
That may seem strange at a first glance, but it appears to be rather convenient.
|
||||
How does it work? Simple.
|
||||
Given those interfaces, DOM generates all code for accessing correct sub-tags and creating model elements at runtime.
|
||||
It is also important to note that with this scenario we avoid potential `NullPointerException`: our DOM guarantees that every method accessing a tag child will return a not-null element, even if the correspondingly-named sub-tag doesn't exist.
|
||||
That may seem strange at first glance, but it appears to be rather convenient.
|
||||
How does it work?
|
||||
Simple.
|
||||
Given those interfaces, DOM generates all the code for accessing correct subtags 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.
|
||||
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 a UI editor for DOM model elements.
|
||||
|
||||
## Building the Model
|
||||
|
||||
@ -154,9 +161,9 @@ 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.
|
||||
For example, the CMP version in EJB may be "1.x" or "2.x", but you can't create Java enums with such names.
|
||||
For such cases, let your enum implement `NamedEnum` interface, and then name your enum elements as you wish.
|
||||
Now, just provide the `getValue()` implementation that will return the right value to match with XML contents, and voilà\!
|
||||
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:
|
||||
|
||||
```java
|
||||
@ -177,7 +184,7 @@ enum CmpVersion implements NamedEnum {
|
||||
```
|
||||
|
||||
As we have already mentioned, an XML tag may have lots 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.
|
||||
Of course, such tags also need a DOM element to associate with.
|
||||
And we provide such an element:
|
||||
|
||||
```java
|
||||
@ -196,15 +203,16 @@ interface GenericDomValue<T> {
|
||||
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`](upsource:///java/java-psi-api/src/com/intellij/psi/PsiClass.java).
|
||||
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.
|
||||
It would be useful to highlight invalid values in the 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
|
||||
|
||||
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:
|
||||
So it's natural to create something like `GenericDomValue<T>` and then work as usual.
|
||||
"Something like" will be an inheritor, as shown below:
|
||||
|
||||
```java
|
||||
interface GenericAttributeValue<T> extends GenericDomValue<T> {
|
||||
@ -219,7 +227,8 @@ Consider that you want to work with an attribute named _some-class_ having a val
|
||||
GenericAttributeValue<PsiClass> getMyAttributeValue();
|
||||
```
|
||||
|
||||
That's all\! Now you can get/set values, resolve this `PsiClass`, get its `String` representation, etc.
|
||||
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:
|
||||
@ -258,7 +267,7 @@ 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 sub-tag's presence that means something, rather than its content --- `<unchecked>` in EJB method permissions, for example.
|
||||
Sometimes it is the sub-tag'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.
|
||||
@ -267,9 +276,10 @@ In this case, `getValue()` will return `true` if the tag exists and `false` othe
|
||||
|
||||
Let's consider another interesting example inspired by EJB, where there is 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.
|
||||
So, we could create a collection of role elements, and every time we access some role we would check if this collection has a 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\!
|
||||
So why can't we have a fixed (more than one) number of children with the same tag name?
|
||||
Let's have them!
|
||||
|
||||
```java
|
||||
@SubTag(value = "ejb-relationship-role", index = 0)
|
||||
@ -279,9 +289,9 @@ EjbRelationshipRole getEjbRelationshipRole1();
|
||||
EjbRelationshipRole getEjbRelationshipRole2();
|
||||
```
|
||||
|
||||
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.
|
||||
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 a 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.
|
||||
@ -292,7 +302,7 @@ For children of [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij
|
||||
|
||||
One more common case in DTD and Schemas 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, the method name must be in pluralized form.
|
||||
For example, in EJB we would have the following method:
|
||||
For example, in the EJB we would have the following method:
|
||||
|
||||
```java
|
||||
List<Entity> getEntities();
|
||||
@ -301,15 +311,15 @@ List<Entity> getEntities();
|
||||
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`).
|
||||
To delete an element from a collection, just call `undefine()` on this element.
|
||||
The tag will then be removed, and an 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 can't just pass some of your DOM elements to some method, to add the element to the collection.
|
||||
Adding elements is a bit harder.
|
||||
Since all DOM elements are created internally, you can't 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:
|
||||
In our example, it's done in the following way:
|
||||
|
||||
```java
|
||||
Entity addEntity(int index);
|
||||
@ -338,7 +348,7 @@ List<Foo> getFoos();
|
||||
List<Bar> getBars();
|
||||
|
||||
// all <foo> and <bar> elements
|
||||
@SubTagsList({"foo", "bar"})
|
||||
@SubTagsList({ "foo", "bar" })
|
||||
List<FooBar> getMergedListOfFoosAndBars();
|
||||
```
|
||||
|
||||
@ -347,8 +357,11 @@ The annotation here is mandatory - we cannot guess several tag names from one me
|
||||
To add elements to such mixed collections, you should create "add" methods for each possible tag name:
|
||||
|
||||
```java
|
||||
@SubTagsList(value={"foo","bar"}, tagName="foo") Fubar addFoo();
|
||||
@SubTagsList(value={"foo","bar"}, tagName="bar") Fubar addBar(int index);
|
||||
@SubTagsList(value = { "foo", "bar" }, tagName = "foo")
|
||||
FooBar addFoo();
|
||||
|
||||
@SubTagsList(value = { "foo", "bar" }, tagName = "bar")
|
||||
FooBar addBar(int index);
|
||||
```
|
||||
|
||||
The index parameter in the last example means the index in the merged collection, not in the collection of tags named "bar".
|
||||
@ -356,7 +369,8 @@ The index parameter in the last example means the index in the merged collection
|
||||
### Dynamic Definition
|
||||
|
||||
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`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/reflect/DomExtensionsRegistrar.java) provides various methods to register dynamic attributes and children.
|
||||
Register it in "extenderClass" attribute of EP `com.intellij.dom.extender`, where "domClass" specifies DOM class `<T>` to be extended.
|
||||
[`DomExtensionsRegistrar`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/reflect/DomExtensionsRegistrar.java) 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()`.
|
||||
|
||||
@ -371,9 +385,9 @@ Output correctness/completeness will largely depend on the input scheme and may
|
||||
|
||||
Follow these steps:
|
||||
|
||||
* Run IntelliJ IDEA with _Plugin DevKit_ enabled in [internal mode](enabling_internal.md)
|
||||
* Run IntelliJ IDEA with _Plugin DevKit_ enabled in the [internal mode](enabling_internal.md)
|
||||
* Select <menupath>Tools | Internal Actions | DevKit | Generate DOM Model</menupath>
|
||||
* Select Scheme file and set options, then click "Generate" to generate sources
|
||||
* Select <control>Scheme File</control> and set options, then click <control>Generate</control> to generate sources
|
||||
* Modify generated sources according to your needs
|
||||
|
||||
### IDE Support
|
||||
@ -390,10 +404,12 @@ _Plugin DevKit_ supports the following features for working with DOM related cod
|
||||
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`).
|
||||
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 (according to the given tag).
|
||||
In such cases, one should decide which interface the DOM element should actually implement (according to the given tag).
|
||||
This is achieved by extending the [`TypeChooser`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/TypeChooser.java) abstract class:
|
||||
|
||||
```java
|
||||
@ -420,14 +436,15 @@ We remember that in `GenericAttributeValue` there's also the `getXmlAttribute()`
|
||||
In general case there is `getXmlElement()` method.
|
||||
You can also get a DOM element by its underlying XML PSI element using the `DomManager.getDomElement()` method.
|
||||
|
||||
If DOM element has no underlying XML element, it can be created by calling `ensureTagExists()`.
|
||||
If a 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.
|
||||
If the element was a collection's child, then neither it nor its entire subtree will be valid anymore.
|
||||
|
||||
#### Tree Structure
|
||||
|
||||
In every normal tree there's always a possibility to walk up. `DomElement` is no exception.
|
||||
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.
|
||||
@ -439,7 +456,7 @@ Finally, `getRoot()` will return the `DomFileElement`, which is the root of ever
|
||||
|
||||
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.
|
||||
They can become invalid only if they have a collection tree ancestor that has been deleted.
|
||||
|
||||
Newly created DOM elements are always correct and valid, so their `isValid()` methods will return `true`.
|
||||
|
||||
@ -457,7 +474,7 @@ There's also `DomElement.getXmlElementName()` method that returns the name of a
|
||||
<!-- TODO: using @Presentation -->
|
||||
|
||||
`DomElement.getPresentation()` returns an instance of [`ElementPresentation`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentation.java), 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`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentationManager.java) as early as possible.
|
||||
Presentations are actually obtained from presentation factory objects that, like `ClassChooser`s, should be registered in [`ElementPresentationManager`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ElementPresentationManager.java) 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`.
|
||||
@ -472,9 +489,9 @@ DOM supports the following events: tag value changed, element defined/undefined/
|
||||
#### Highlighting Annotations
|
||||
|
||||
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).
|
||||
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`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/highlighting/DomElementsAnnotator.java) 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()`).
|
||||
In `DomElementsAnnotator.annotate(DomElement element, DomElementsProblemsHolder annotator)` you should report about all errors and warnings in the element's subtree to the annotator (`DomElementsProblemsHolder.createProblem()`).
|
||||
You should return this annotator in the corresponding virtual method of the `DomFileDescription`.
|
||||
|
||||
#### Automatic Highlighting (BasicDomElementsInspection)
|
||||
@ -494,13 +511,15 @@ To suppress spellchecking annotate your DomElement with `@com.intellij.spellchec
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
### Resolving
|
||||
|
||||
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.
|
||||
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`.
|
||||
|
||||
@ -509,7 +528,7 @@ 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`.
|
||||
The converter used is [`DomResolveConverter`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/DomResolveConverter.java).
|
||||
Its constructor takes a parameter, so it can't be referenced in `@Convert` annotation, but its subclasses (if you create them) — can.
|
||||
Its constructor takes a parameter, so it can't 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()`.
|
||||
|
||||
@ -519,39 +538,45 @@ If you want to create a custom converter and want to have this code insight with
|
||||
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`](upsource:///platform/core-api/src/com/intellij/psi/PsiReference.java) will recognize the similarities here.
|
||||
|
||||
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.
|
||||
If you need to choose a `Converter` depending on other values (e.g. in a sibling/parent element) or any runtime condition (e.g. presence or version of a library), you can use `WrappingConverter`.
|
||||
See also `GenericDomValueConvertersRegistry` for managing an extensible registry of available Converters to choose from.
|
||||
|
||||
### Mock and Stable Elements
|
||||
|
||||
Your DOM elements do not have to be tied to a physical file. `DomManager.createMockElement()` will help you to create a virtual element of a given class with the given module.
|
||||
An element may be physical or not. 'Physical' here means that DOM will create a mock document for it, so you can enjoy Undo functionality if you pass this document to the right place in file editor.
|
||||
Your DOM elements do not have to be tied to a physical file.
|
||||
`DomManager.createMockElement()` will help you to create a virtual element of a given class with the given module.
|
||||
An element may be physical or not.
|
||||
'Physical' here means that DOM will create a mock document for it, so you can enjoy <control>Undo</control> functionality if you pass this document to the right place in the 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.
|
||||
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 Platform's XML parser is incremental: changes in text do not cause the whole file to be reparsed.
|
||||
IntelliJ Platform's XML parser is incremental: changes in a text do not 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)`.
|
||||
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".
|
||||
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 following 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.
|
||||
* `DomElement getWrappedElement()` - just returns the current element to which all method calls are delegated;
|
||||
* `void invalidate()` - makes the wrapped element invalid. Any following 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
|
||||
|
||||
@ -559,9 +584,12 @@ 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) aren't known to anyone except you.
|
||||
But when you instantiate the `DomElementVisitor` interface, you may add there these `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:
|
||||
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) aren't known to anyone except you.
|
||||
But when you instantiate the `DomElementVisitor` interface, you may add there these `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:
|
||||
|
||||
```java
|
||||
class MyVisitor implements DomElementVisitor {
|
||||
@ -605,7 +633,7 @@ Simply register it using extension point `com.intellij.dom.implementation` and D
|
||||
### Models Across Multiple Files
|
||||
|
||||
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.
|
||||
Depending on implementation/plugin, providing filesets implicitly (using existing framework's setup in a project) or via user configuration (usually via dedicated `Facet`) can be achieved.
|
||||
|
||||
Extend [`DomModelFactory`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/model/impl/DomModelFactory.java) (or [`BaseDomModelFactory`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/model/impl/BaseDomModelFactory.java) 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.
|
||||
@ -631,7 +659,7 @@ Set and increase `stubVersion` of `com.intellij.dom.fileMetaData` extension when
|
||||
|
||||
All 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.
|
||||
The former is called resetting, the latter - committing.
|
||||
There's [`Committable`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/Committable.java) 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`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/CompositeCommittable.java).
|
||||
Methods `commit()` and `reset()` are invoked automatically on editor tab switch or undo.
|
||||
@ -639,13 +667,14 @@ So you only need to ensure that all your Swing structure is organized in a tree
|
||||
|
||||
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.
|
||||
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 control's 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`.
|
||||
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.
|
||||
@ -655,10 +684,16 @@ This method will be invoked inside the same write action as the main `commit()`,
|
||||
|
||||
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 object should implement the `DomWrapper` interface that knows how to set/get values to/from a DOM model.
|
||||
|
||||
We have three major DomWrapper's: [`DomFixedWrapper<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomFixedWrapper.java) redirecting calls to [`GenericDomValue<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java), [`DomStringWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomStringWrapper.java) redirecting calls to string accessors of [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java), and [`DomCollectionWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomCollectionWrapper.java) that gets/sets values of the first element of the given [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java) 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 have three major DomWrapper's: [`DomFixedWrapper<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomFixedWrapper.java) redirecting calls to
|
||||
[`GenericDomValue<T>`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java),
|
||||
[`DomStringWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomStringWrapper.java)
|
||||
redirecting calls to string accessors of
|
||||
[`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java),
|
||||
and [`DomCollectionWrapper`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomCollectionWrapper.java)
|
||||
that gets/sets values of the first element of the given [`GenericDomValue`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/GenericDomValue.java) collection.
|
||||
Some controls (those having a text field as part of itself) take an 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)`.
|
||||
@ -687,11 +722,11 @@ Or you can just delegate to that renderer in your own way.
|
||||
|
||||
##### BooleanEnumControl
|
||||
|
||||
Sometimes, when there are only 2 alternatives, it's convenient to use a check box instead of combo box.
|
||||
Sometimes, when there are only 2 alternatives, it's convenient to use a checkbox instead of a combobox.
|
||||
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.
|
||||
While being (and being bound to) a checkbox, 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.
|
||||
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.
|
||||
|
||||
### Editor-Based Controls
|
||||
@ -703,7 +738,7 @@ Since there's currently no way to instantiate Editor directly through the Open A
|
||||
|
||||
This control allows you to edit simple string values.
|
||||
The control is bound to a `TextPanel` component.
|
||||
There's also an inheritor of that panel — `MultiLineTextPanel`.
|
||||
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.
|
||||
@ -726,9 +761,10 @@ 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()`.
|
||||
This can be passed to the constructor, or can be created in a `DomCollectionControl` inheritor, in an overridden method `createColumnInfos()`.
|
||||
|
||||
What is a column info? It's just a somewhat more comfortable way to work with the table model.
|
||||
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.
|
||||
@ -742,13 +778,14 @@ 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.
|
||||
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 Platform'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.
|
||||
`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`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/DomTableView.java).
|
||||
@ -777,9 +814,9 @@ The easiest way to create a DOM-based UI form is to extend the
|
||||
[`BasicDomElementComponent`](upsource:///xml/dom-openapi/src/com/intellij/util/xml/ui/BasicDomElementComponent.java) 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.
|
||||
You will surely want to bind some controls to DOM UI, in which case you should of course ensure that they have the 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.
|
||||
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.
|
||||
@ -808,9 +845,9 @@ public class ConverterComponent extends BasicDomElementComponent<Converter> {
|
||||
}
|
||||
```
|
||||
|
||||
All the fields here are now bound to controls in a GUI form.
|
||||
All the fields here are now bound to the controls in the GUI form.
|
||||
|
||||
Very often you'll have to create your own file editor.
|
||||
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`](upsource:///platform/analysis-api/src/com/intellij/openapi/fileEditor/FileEditorProvider.java)
|
||||
from
|
||||
|
Loading…
x
Reference in New Issue
Block a user