-
Notifications
You must be signed in to change notification settings - Fork 31
WindowBuilder Designer 2.0 Specifications
We should understand, that it is impossible to parse all existing code. Yes, it may be works at runtime, in other GUI builders, etc. But we should know who are our typical customers, how they work and what they typically need. We should balance features with complexity of Designer's code.
Parsing begins from well defined «entry points», different for each GUI element, for example constructor for SWT Composite. Parsing continues following by directly/unconditionally accessible code, so excludes any «if» statements (both then/else branches), loops, code defined in event handlers or methods that are not invoked from «entry point». For example, in code below we execute «statement», «block» and method «createButtonsGroup», but don't execute «condition» and «loop». I.e. in contrast to the parser in «Designer 1.0» we process code not because we «can see it», but because we «can execute it».
public class MyComposite extends Composite {
public MyComposite(Composite parent, int style) {
super(parent, style);
// statement
setLayout(new GridLayout());
// block
{
Text text = new Text(this, SWT.BORDER);
text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
}
// method invocation
createButtonsGroup(this);
// condition
if (System.currentTimeMillis() > 0) {
new Button(this, SWT.NONE).setText("Conditional button");
}
// loop
for (int i = 0; i < 10; i++) {
new Button(this, SWT.NONE).setText("i = " + i);
}
}
private void createButtonsGroup(Composite parent) {
Group group = new Group(parent, SWT.NONE);
group.setLayout(new GridLayout(2, false));
{
Button button = new Button(group, SWT.NONE);
button.setText("Button 1");
}
{
Button button = new Button(group, SWT.NONE);
button.setText("Button 2");
}
}
}Creation patterns describe structures of code that Designer treats as creation of GUI components. These patterns can be separated on three sets: should be supported, can be supported with limitations and hard to support.
Constructors are most usual way for creating components, so we should support them as good as possible. Any public constructor with any number of arguments of any types should be supported, however with possible specific limitations for GUI toolkits.
For example SWT requires parent and style to be passed as constructor's argument, so we require org.eclipse.swt.Composite and int as first and second arguments of constructor. In future, if we will add some DSL for describing components, we can relax this limitation by allowing to specify parent as any constructor argument.
Arguments of constructor with good known types (numbers, string and other, if we have specific editor for them) should be displayed in “property table”.
Example: new Button(group, SWT.NONE)
Static factories are much like constructors except that method invocation instead of class instance creation is used. So, they should have same set of features.
Example:
public final class ComponentsFactory {
public static JButton createButton(String text) {
JButton button = new JButton(text);
button.setForeground(Color.red);
return button;
}
}
public class ComponentsFactoryTest extends JFrame {
public ComponentsFactoryTest() {
super();
getContentPane().setLayout(null);
setBounds(100, 100, 650, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//
JButton button = ComponentsFactory.createButton("My button");
getContentPane().add(button);
button.setBounds(100, 100, 100, 100);
}
}Sometimes we want to create some custom component that consists of several sub-components exposed using getters. For example:
public class TitlePanel extends JPanel {
private final JLabel m_label = new JLabel();
private final JPanel m_content = new JPanel();
public TitlePanel() {
setLayout(new BorderLayout());
//
add(m_label, BorderLayout.NORTH);
m_label.setOpaque(true);
m_label.setBackground(Color.ORANGE);
m_label.setText("New JLabel");
//
add(m_content, BorderLayout.CENTER);
}
public JPanel getContent() {
return m_content;
}
public String getTitle() {
return m_label.getText();
}
public void setTitle(String text) {
m_label.setText(text);
}
}
public class TitlePanelTest extends JFrame {
private final JButton m_button = new JButton();
private final TitlePanel m_titlePanel = new TitlePanel();
public TitlePanelTest() {
getContentPane().setLayout(null);
setBounds(100, 100, 650, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//
getContentPane().add(m_titlePanel);
m_titlePanel.setTitle("My title");
m_titlePanel.setBounds(131, 97, 241, 203);
//
m_titlePanel.getContent().add(m_button);
m_button.setText("New JButton");
}
}For such “exposed” component we can change properties. However there are limitations on possible operations:
- delete – should be handled as removing all added children and property modifications;
- set layout (for containers) – only if there are no children (default, i.e. not added);
- add new child – only if supported by layout, for example absolute layout can allow this, but GridBagLayout - no.
Components creation using lazy creation (VE like).
public class LazyTest extends JFrame {
private JButton m_button;
public LazyTest() {
setBounds(100, 100, 650, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().add(getButton(), BorderLayout.NORTH);
}
protected JButton getButton() {
if (m_button == null) {
m_button = new JButton();
m_button.setText("New JButton");
}
return m_button;
}
}Here we should create new component instance using “new JButton()”, or may be any other pattern, but treat getButton() as special way for accessing component, same as field. Hm... may be this should be moved into “variable patterns”.
MenuItem item = Menu.addItem(“text”);
There are components that can be created just as method invocations for other components, for example:
public class ToolbarSeparatorTest extends JFrame {
public ToolbarSeparatorTest() {
setBounds(100, 100, 650, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//
final JToolBar toolBar = new JToolBar();
getContentPane().add(toolBar, BorderLayout.NORTH);
//
toolBar.addSeparator();
}
}Here “toolbar.addSeparator()” adds new component, but this component can not be assigned to variable, so we don't know its class and can not set most properties (only properties for method arguments).
Example:
public interface IComponentsFactory {
JButton createButton(String label);
}
public class RedComponentsFactory implements IComponentsFactory {
public JButton createButton(String label) {
JButton button = new JButton(label);
button.setForeground(Color.red);
return button;
}
}
public class RedComponentsFactoryTest extends JFrame {
private final IComponentsFactory m_componentsFactory = new RedComponentsFactory(); // Designer should be able to create factory instance
public RedComponentsFactoryTest() {
super();
setBounds(100, 100, 650, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//
getContentPane().add(m_componentsFactory.createButton("Add..."), BorderLayout.NORTH);
}
}To create components we should be able to create instance of factory, so it should be directly initialized or may be we should support special comment that will specify how to create instance of factory for design time, because for example factory should be passed externally at runtime.
Any methods that executed from several points and/or create several components.
Component instance in theory can be assigned to several variables (local or fields). We should treat one of them as “main” variable and use it for: constructing “reference” and “access” expressions, conversion to field/local, rename operations, etc. Sometimes component don't have variable, in that case request for “reference”/”access” expression will generate new, unique variable.
Possible variable states and operations:
| Case description | SetVariable | Reference expression, local | Reference expression, remote | |
|---|---|---|---|---|
| 1 | In block, unique | rename variable | Return variable | Generate unique field |
| 2 | In method, unique | rename variable | Return variable | Generate unique field |
| 3 | In method, not unique | Generate unique variable based on given name | Return variable | Generate unique field |
| 4 | In field, assignment in method, unique | rename field | Return field | Return field |
| 5 | In field, assignment in declaration, unique | Rename field | Return field | Return field |
| 6 | In field, no assignment on declaration, several assignments in methods | Generate unique field based on given name | Return field | Generate unique field |
| 7 | In field, assignment in declaration, several assignments in methods | Same as 6 | Same as 6 | Same as 6 |
- “Local” expression means that we will use it to add method invocation directly after place where object is assigned to variable.
- “Remote” expression means that we will use it potentially outside of block where object is assigned to variable, for example in GroupLayout we add all components at the end of method.
- “Variable” of component is variable to which object is assigned first time.
Custom components with custom property editor often use complex expressions to as values of properties. These expressions include creation of new objects using constructors, arrays, numbers, strings, etc. To support them we need some way for evaluating these expressions and converting them into values. Main limitation here is that expression should be «statically evaluable», i.e. reference only variables/fields which values can known without running code. This mean practically that we can start only from static values/methods, however these static values/methods can be instances of objects.
One problem in «Designer 1.0» is handling of «null» value. It was used as «unknown value», but this causes problem in following example:
new MyData(null, 0);
public class MyData {
private final String m_title;
private final int m_value;
public MyData(String title, int value) {
m_title = title;
m_value = value;
}
public String getTitle() {
return m_title;
}
public int getValue() {
return m_value;
}
}Here «null» is just value passed to the constructor. But if we will evaluate it in general way, we can not distinguish it from «unknown value», so we fail to create object and in general evaluate full expression. To solve this problem, explicit «unknown value» should be defined and used across all Designer.
In D1, the live component tree was created recursively from the root. First, an empty object was created, then for each property, we tried to find a corresponding setXXX(value) in related nodes. If found, we called this method on the created object.
The number of related node searches is linearly dependent on the number of properties, but the coefficient is definitely not 1; at best, 2, and more likely 3+. After the object was fully created, we called the creation of child objects. Next, LayoutManagerInfo (for Swing) was responsible for parsing constraints and calling the appropriate add(component, constraints).
D2 uses a different approach. Instead of heroically extracting each individual property from related nodes, we simply execute the corresponding setXXX(value). We don't even look at related nodes; they are used only as a cache to avoid traversing the entire AST when the user changes an individual property. In general, the new operating principle is as follows:
- the result of the Designer's work is Java source;
- The Java source is an AST;
- To display the GUI, the AST must be executed. Moreover, the AST itself will be used not only for properties, but also for the actual object creation. If it says to call a constructor, the constructor will be called—the specified one, not the default one, with manual assignment of parameters to properties.
Not the entire AST should be executed, or even its entire execution flow, but only component creation and MethodInvocation for components (plus probably FieldAccess). In this case, we must add two hooks for expression evaluation:
-
for successful evaluation, and if this was a successful evaluation of component creation (where such an expression exists, say, for constructor creation, but not for the exposed component), then remember it as an object for this model;
-
for the need to evaluate a certain expression—we must check that if the expression is a reference to one of the JavaInfo objects in the tree, then the previously remembered object for it must be returned. This algorithm prevents duplicate component creation. We can also use this same technique to cache the values of other Expressions. It's worth considering whether this is only for the duration of the current refresh() or for the entire duration of the CompilationUnit.
Overall, I think the new approach is no worse than the previous one. It allows, for example, to describe additional methods that should also be executed for components, not just setXXX(). However, the Designer might not understand what these methods mean, not display them in properties, and not support editing. But at least we give the user the ability to manually add the required method call in the source code. Although I have plans to allow for extensive editing using component descriptors in XML format.
In D1, supporting each such component, even a simple JPanel, is a huge pain: a separate child, copies of methods like getStatement(), getAccessExpression(), etc.
In D2, there's a desire to implement universal support using a dedicated ThisCreationSupport. In the case of ConstructorCreationSupport, a method for creating a component seems unnecessary given that we've decided not to create objects manually, but simply interpret the AST. For "this" components, we'll still have to provide the ability to create components in CreationSupport. The difficulty with "this" is that there's no ASTNode for it in which to create it. Such an ASTNode could, in principle, be defined as a call to super(), but our ASTEvaluationEngine definitely can't (and shouldn't!) evaluate super().
It seems reasonable to think that we can simply create an instance of the ancestor using the constructor that's called as super(). But there's a problem: parameters that come into the constructor of the child we're editing might be passed there. A possible solution is to give the user a way to describe the parameter values that should be used. This is best done as a comment above or below super(). It's even possible that not all parameters need to be described, but only those that can't be statically calculated. There will be limitations, of course, but for most tasks, the possibilities will be greatly expanded.
Another problem is inheriting from an abstract ancestor. Currently (even in D2), we create a URLClassLoader for the project class path. We could use a custom ClassLoader that looks up classes through ASM and replaces abstract methods with methods that throw an exception. This way, if we don't need to call these methods, we can instantiate the object and work with it. If we do need to—well, we've tried; there's not much we can do here. Although it would still be possible to expand the capabilities even further and make it possible to describe in XXX.wbp-component.xml that these abstract methods can be ignored (let's say, there is event processing there), and from these ones, a certain value can be returned.
Sometimes we need to evaluate expressions that are simply specified as strings, rather than as part of a compilation unit that we can convert into an AST. For example, specifying parameter values for super(). It might be possible to solve this problem by creating a fake compilation unit, an AST for it, evaluating the expression, and carefully discarding the AST.
I think the main tasks of the Designer are the following:
- Visualize forms existing in the source code.
- Find all JavaInfo created during the execution flow.
- Create JavaInfo for any objects accessible from explicitly (or implicitly) created JavaInfo using the getXXX() methods (or any others described in the descriptors), the so-called exposed components.
- Associate JavaInfo in parent/child relationships. Including selecting a "logical parent" for each exposed component.
- Traverse the AST during the execution flow and interpret expressions, executing not all JavaInfo methods, but only those allowed in the descriptors.
- Modify existing source code.
- Modify JavaInfo properties.
- A property is a value that we allow the user to edit. Typically, this value is displayed in some way in the AST.
- The property has: a title, a default value, a current value, text displayed to the user, and a flag indicating whether the current value exists.
- The property can be: modified, reset to its default value (usually this means removing it from the AST, but can also simply be set to a default value in the AST).
- Adding new JavaInfos.
- Removing existing JavaInfos.
- Deleting existing JavaInfos.
- Modify JavaInfo properties.
- Figure out how to describe which methods can be executed. I think this should be done using templates based on the method signature. Methods themselves are nonsense; you could describe how to execute all setXXX() methods plus specific methods with multiple arguments, but that's more for users.
- Figure out how to describe properties.
- In particular, properties created by methods, while other properties are highly specific and unlikely to ever be included in descriptors.
- But even with methods, there are many possibilities. For example, methods with multiple arguments—when setting a single property, all arguments must be filled in. Most likely, for such methods, default values for each argument should be described.
- Default values for properties by methods included by templates. Actually, the question is: should they be included by methods? Or should methods be included separately, and properties separately? Ideally, they're related, since properties are a way to edit methods.
- The main problem is with properties.