Author:
Jiri Skrivanek
Last update: April 18, 2005
Writing Jelly Operators Guide
Introduction
This document describes how to write module-specific Jelly classes
which will cover non core IDE parts.
Jellytools provide objects in architecture which is easy to
understand and maps to IDE object model. It helps people familiar with
IDE to understand Jellytools well. Jellytools offer APIs
for most of the operations required by tests. In the same time it
allows
tests to be extended beyond Jellytools API capabilities without
"hacking". Users can see ways how to go beyond Jellytools as
really supported and clear.
Jellytools are based on Jemmy that means majority of Jelly classes
extends Jemmy operator what brings advantage that Jelly operators can
use all methods already implemented in Jemmy. That's why we will call
classes which extends Jemmy operator "Jelly operators".
The document will guide you through several areas of operators
creation. It describes where to store operators, how to name them,
inner
structure of Jelly operator and how to handle internationalization. An
extra attention is paid to nodes, actions, properties and wizards.
CVS structure
The root of package hierarchy is
org.netbeans.jellytools.
Operators and components serving functionality belonging to IDE's core
and openide are under root package org.netbeans.jellytools. Also
utilities and other stuff go there.
There exist special packages dedicated to extra functionality -
actions, nodes and properties. Package
actions holds
implementation of possible node actions like CopyAction. Package
nodes
includes classes to manipulate tree nodes like FolderNode. Package
properties
contains stuff related to IDE's property sheet and sub package
editors
for custom editor operators.
Module specific operators are stored in
jellytools.modules.<moduleName>
package. Under that package can be repeated structure of root package -
actions, nodes, properties.
See the following schema where packages are in bold.
org.netbeans.jellytools
- actions
- Action
- ActionNoBlock
- CopyAction
- DeleteAction
- modules
- form
- actions
- nodes
- properties
- editors
- FormCustomEditorAdvancedOperator
- ComponentInspectorOperator
- javacvs
- nodes
- properties
- editors
- PointCustomEditorOperator
- PointProperty
- Property
- PropertySheetTabOperator
- Bundle
- MainWindowOperator
- NbDialogOperator
- ProjectsTabOperator
- TopComponentOperator
- WizardOperator
Class naming conventions
1) All classes which extends a Jemmy or Jelly operator should have
suffix "Operator" (e.g. MainWindowOperator).
2) "Nb" prefix should be used, if there is a need to distinguish IDE
specific operators. Additionally it can be used,
if IDE's peer component has such prefix (e.g. NbDialog <->
NbDialogOperator).
3) Classes for actions have "Action" suffix (e.g. CopyAction).
4) Classes for nodes have "Node" suffix (e.g. JavaNode).
5) Classes for properties have "Property" suffix (e.g.
PointProperty).
6) Classes for set of properties have "Properties" in the middle
(e.g. XMLPropertiesOperator)
Jelly operator
Every Jelly operator represents an existing IDE component which can
include some sub components. Typical example of such component is a
dialog with buttons, text fields, etc. Jelly operator can be inherited
from one of the following Jemmy operators:
- ComponentOperator
- ContainerOperator
- DialogOperator
- FrameOperator
- JComponentOperator
- JDialogOperator
- JFrameOperator
- JInternalFrameOperator
- WindowOperator
It is highly recommended that module-specific operator extends one of
the following Jelly operators:
- NbDialogOperator
- TopComponentOperator
- WizardOperator
NbDialogOperator is generic ancestor for majority of IDE dialogs.
NbDialogOperator is extended by WizardOperator which helps to implement
operators for wizards. TopComponentOperator represents any dockable
view which can reside inside a host frame
Sub component can be an atomic Swing or AWT component or a compound
component. Peers for atomic components are Jemmy operators and for
other
components Jelly operators, if they exist.
Implementation should follow Java
code conventions. All non private methods and fields should have
javadoc comments. See NbDialogOperator
or SaveAsTemplateOperator
as examples.
Definition of Jelly operator class consist of several parts:
Sub components variables declaration
Name to identify a sub component should be the same as subcomponent
text whenever it is possible (e.g. Cancel for a cancel button).
Otherwise it can be text of label associated with it or other suitable
name. Sub component belonging to the same logical group should have the
same prefix in the variable name (see
table
of
prefixes).
Every sub component operator should be declared as private.
Variable name: "_"+prefix+name (e.g. _btCancel)
Variable declaration example:
private JButtonOperator _btCancel;
Also known constants should be declared here. For example, possible
items in combo box:
public static final String ITEM_FIRST_OPTION
= Bundle.getString("my.Bundle", "FirstOption");
Constructors
Constructor of a component should wait for that component (not invoke
it). This happens by call of super() with appropriate parameters like
this:
public NbDialogOperator() {
super();
}
There can exist several constructors, if there is a good reason for
that, for example title of a window may change:
public NbDialogOperator(String title) {
super(title);
}
Invocation
In some cases it is possible to define a way how a component can be
invoked. For example, Options window can be opened by main menu Tools
-> Options, Save As Template dialog by popup menu on a node. Jelly
operator should include invoke() method which brings component up and
returns instance of appropriate operator. It is recommended to use
pre-defined actions. Method invoke() can be without parameters or can
accept parameters like in the following examples:
public static SaveAsTemplateOperator
invoke(Node node) {
return invoke(new Node[]
{node});
}
public static SaveAsTemplateOperator
invoke(Node[] nodes) {
new
SaveAsTemplateAction().perform(nodes);
return new
SaveAsTemplateOperator();
}
Sub components getters
Every sub component have to have getter to get internal instance of
corresponding operator. It is highly recommended to use lazy
initialization here, i.e. initialize operator right before than it is
used.
Method name: prefix+name (e.g. btCancel())
Method example:
public JButtonOperator btCancel() {
if (_btCancel == null) {
String cancelCaption = Bundle.getString("org.netbeans.core.Bundle",
"CANCEL_OPTION_CAPTION");
_btCancel = new JButtonOperator(this, cancelCaption);
}
return _btCancel;
}
Low-level methods
For some sub components can be added shortcuts to frequently used
methods. See
table below for required
methods to a particular operator.
Method name: name with first char in lower case (e.g. cancel()) or with
prefix "set"
Method examples:
public void cancel() {
btCancel().push();
}
public void setMyTextField(String text) {
txtMyTextFiled().clearText();
txtMyTextFiled().typeText(text);
}
Verify method
Method verify() calls getters for all sub components. In fact it checks
if all sub components are present in operator's container.
public void verify() {
lblSelectTheCategory();
tree();
btOK();
btCancel();
}
Business logic methods
Such methods perform complex actions with sub components in single
call. User of Jelly doesn't need to worry about all UI manipulation
necessary to perform such operations. An example of business logic
method is above verify() method which does verification of all sub
components. Another example can be filling a form "at once", setting
all
components in a wizard's panel, etc.
Method example:
public void selectFileType(String filetype) {
lstFileTypes().selectItem(filetype);
}
I18N
Jelly operators should not contain any hard coded strings. All string
values should be extracted from IDE bundles using
org.netbeans.jellytools.Bundle class like this:
// returns string "Cancel"
Bundle.getString("org.netbeans.core.Bundle",
"CANCEL_OPTION_CAPTION");
If a label is compound from optional pieces, parameters can be
supplied:
// returns string "Properties of AnObject"
Bundle.getString("org.netbeans.core.Bundle",
"CTL_FMT_LocalProperties", new Object[] {new Integer(1), "AnObject"});
Sometimes a string from Bundle contains '&' or optional
parameter place holders '{0}. If you want to cut them use the
following:
// returns string "View" (originally "&View")
Bundle.getStringTrimmed("org.netbeans.core.Bundle",
"Menu/View");
Usage of bundles will satisfy usability of jellytools library on
different than English locale. It is also recommended to use such
approach in test cases which are intended to be executed on several
locales.
Jelly nodes
Nodes should enable easier navigation inside a tree and easier
manipulation with tree items. A node can be used as a parameter for an
action and a particular implementation of node should include methods
to
perform all possible actions on the node. You can look at
implementation of
JavaNode
as an example.
Constructor - just call parent's constructor with appropriate
parameters
public JavaNode(Node parent, String
treeSubPath) {
super(parent, treeSubPath);
}
Override all parent's constructors and optionally add your own
costructor, for example when you assume where to find node:
public SourcePackagesNode(String
projectName) {
super(new
ProjectsTabOperator().getProjectRootNode(projectName),
SOURCE_PACKAGES_LABEL);
}
Action definitions - all possible actions should be declared
as static final and an instance should be immediately
created:
static final OpenAction openAction = new
OpenAction();
static final CopyAction copyAction = new
CopyAction();
Methods to perform action - for each declared action there
should exist method performing the action on this node
public void open() {
openAction.perform(this);
}
Jelly actions
Actions are intended to hide and re-use main menu, popup menu, shortcut
and IDE API invokation. Ancestor of all actions is class
org.netbeans.jellytools.actions.Action. Its descendant ActionNoBlock is
used as parent of actions which produce blocking modal dialog. There
exist four ways how to perform an action:
- main menu (menu item from IDE main window is pushed)
- popup menu (menu item on a node or on a component is pushed)
- IDE API (IDE system action is called)
- keyboard shortcut (keyboard shortcut combination is pressed)
If there some of them doesn't exist for a particular action, it is not
defined in constructor. By default is action performed in popup mode.
If
a mode is not defined, next available is used.
Method performing an action can consume following parameters:
- without parameters
- node (select node before)
- array of nodes (select nodes before)
- ComponentOperator (focus component before)
If some of methods is not applicable on a particular action, method
throws
UnsupportedOperationException.
If an action is not defined, users can use generic Action
or ActionNoBlock, or they can implement their module-specific
action. See examples in actions
package.
Constructor - call parent's constructor and supply parameters
for all modes in which the action should be used
public CopyAction() {
super(copyMenu, copyPopup,
"org.openide.actions.CopyAction", copyShortcut);
}
Of course use strings from Bundle.properties to declare
menu paths:
private static final String copyPopup =
Bundle.getStringTrimmed("org.openide.actions.Bundle", "Copy");
private static final String copyMenu =
Bundle.getStringTrimmed("org.netbeans.core.Bundle", "Menu/Edit")
+ "|" + copyPopup;
private static final Shortcut copyShortcut
= new Shortcut(KeyEvent.VK_C, KeyEvent.CTRL_MASK);
Use null, if action is not usable in a mode. For example, SaveAllAction
doesn't have popup and shortcut representation:
public SaveAllAction() {
super(Bundle.getString("org.openide.actions.Bundle", "SaveAll"), null,
"org.openide.actions.SaveAllAction");
}
Perform methods - override perform methods if you need a
special algorithm to perform action or if you need to supress wrong
usage
public void performPopup(ComponentOperator
compOperator) {
if(compOperator instanceof
TopComponentOperator) {
performPopup((TopComponentOperator)compOperator);
} else {
throw new UnsupportedOperationException(
"CloneViewAction can only be called on TopComponentOperator.");
}
}
// SaveAction - supress popup usage on node
public void performPopup(Node node) {
throw new
UnsupportedOperationException(
"SaveAction doesn't have popup representation on node.");
}
Jelly properties
In the IDE properties settings happens through so called property
sheets. Property sheet can be docked to any other window. Property
sheet
consists of JTable with properties and optional description area. The
table has two columns showing property name and property value.
Properties are grouped into logical categories which can be collapsed
or
expanded when properties are sorted by category. It is also possible to
sort properties alphabetically (Sort by Name).
Property
Property value can be changed by inline editors (text field for string
values, check box for boolean values -
setValue(String) and combo box
for list values -
setValue(int)
method) or by custom editors where they are available (
openEditor() method).
You should extend Property,
if you want to create your module-specific property. You can look at
implementation of PointProperty
as an example. Property implementation should consist of following
parts:
Constructor - just calls parent's constructor with
appropriate parameters
public PointProperty(PropertySheetOperator
propertySheetOper, String name) {
super(propertySheetOper,
name);
}
Customizer invocation - opens custom editor and returns
appropriate custom editor operator
public PointCustomEditorOperator
invokeCustomizer() {
openEditor();
return new
PointCustomEditorOperator(getName());
}
Set/get values by custom editor - sets or gets values by
property custom editor (open editor, set/get values, close editor).
There can exist several methods with different parameters.
public void setPointValue(String x, String y)
{
PointCustomEditorOperator customizer = invokeCustomizer();
customizer.setPointValue(x, y);
customizer.ok();
}
public String[] getPointValue() {
String[] value = new
String[2];
PointCustomEditorOperator customizer = invokeCustomizer();
value[0] =
customizer.getXValue();
value[1] =
customizer.getYValue();
customizer.cancel();
return value;
}
Your module-specific property inherits also methods getValue(),
setValue(String) and setValue(int) from Property which gets/sets value
without custom editor.
Custom Editor
If there exists special custom editor for property, there appears small
"..." button which invokes such custom editor dialog. Custom editor is
embedded in a dialog. If it is property of a component from Form
Editor,
dialog contains Default, OK and Cancel buttons, combo box enabling to
change editor (e.g. PointEditor/Form Connection), button "Advanced" to
call "Advanced Initialization Code" dialog. Otherwise it is dialog with
OK and Cancel buttons (Default and Help are optional).
To create your module-specific custom editor you should extend
NbDialogOperator (see PointCustomEditorOperator
for instance). Because a custom editor is regular IDE component you
should follow the same rules as for creating any other Jelly operator. It should include
constructor/s, getters for sub components, low-level methods to
manipulate with sub components and high-level methods to set compound
property value in one method. An example of such simple high-level
method can be one from PointCustomEditorOperator:
public void setPointValue(String x, String y)
{
txtFieldX().setText(x);
txtFieldY().setText(y);
}
Group of properties
Properties for a special object can be grouped together to a special
class for easier manipulation. For example, property sheet of an Java
node contains three categories - Properties (Name, ...), Text
(Encoding), Classpaths (Compile Classpath, ...). Class
jellytools.modules.java.properties.JavaPropertiesOperator should embed
all
these categories.
Partial implementation of grouping Properties operator should look
like this:
public class JavaPropertiesOperator extends
PropertySheetOperator
{
public JavaPropertiesOperator(PropertySheetOperator
propertySheetOper) {
super(propertySheetOper);
}
public Property prtName() {
// "Name"
String name =
Bundle.getString("org.openide.loaders.Bundle", "PROP_name");
return new
Property(this, name);
}
public Property prtEncoding() {
// "Encoding"
String name =
Bundle.getString("org.netbeans.modules.java.Bundle",
"PROP_fileEncoding")
return new
Property(this, name);
}
}
Package hierarchy
Common properties and related operators are stored under
org.netbeans.jellytools.properties package. Operators for custom
editors
then comes under properties.editors package. This structure can occur
under every module package.
org.netbeans.jellytools
- properties
- editors
- PointCustomEditorOperator
- StringCustomEditorOperator
- PointProperty
- Property
- PropertySheetOperator
- modules
- <moduleName>
- properties
- editors
- SpecialModuleCustomEditorOperator
- SpecialPropertiesOperator
Jelly wizards
IDE wizards are modal dialogs created by WizardDescriptor. They used to
include list of steps on the left side, buttons Back, Next, Finish,
Cancel, Help on the bottom and a panel with functional components.
WizardOperator provides access to common buttons and the list. It is
descendant of NbDialogOperator which is recommended ancestor for all
IDE
dialogs.
To create your module-specific wizard operator/s you should extend
WizardOperator (see
NewFileWizardOperator
for instance). Your wizard operator will contain constructors,
invoke methods and optionally business logic methods.
Constructor - calls parent's constructor with appropriate
parameter
public NewFileWizardOperator() {
super(Bundle.getString("org.netbeans.modules.project.ui.Bundle",
"LBL_NewFileWizard_Subtitle"));
}
Invokation - define how to open wizard. Use actions
preferably.
public static NewFileWizardOperator invoke() {
new
NewFileAction().perform();
return new
NewFileWizardOperator();
}
Business logic - optionally user can implement methods to go
through wizard but user should not duplicate tests of the wizard here.
Every next step/page of a wizard should be handled by single
operator which extends above mentioned wizard operator. Because a
wizard
dialog is regular IDE component you should follow the same rules as for
creating any other Jelly operator. It
should include constructor/s, getters for functional sub components,
low-level methods to manipulate with sub components and business logic
methods to change state of several components in one method. Its name
should end with StepOperator suffix.
Constructor - calls parent's constructor and checks in the
list of steps whether requested panel is selected
public NewFileNameLocationStepOperator() {
super();
checkPanel(Bundle.getString("org.netbeans.modules.java.project.Bundle",
"LBL_JavaTargetChooserPanelGUI_Name"));
}
Sub components getters - all sub components should have
getter
to obtain instance of appropriate operator
Low-level methods - methods to call sub component operator's
methods
Business logic methods - methods to change state of several
components in a panel in obe method
Sub component operators prefixes and
recommended low-level methods
| Operator |
prefix |
low-level methods |
| AbstractButtonOperator |
abt |
push() |
| ButtonOperator |
bt |
|
| CheckboxOperator |
cb |
|
| ChoiceOperator |
cho |
|
| ComponentOperator |
- |
|
| ContainerOperator |
- |
|
| DialogOperator |
- |
|
| FrameOperator |
- |
|
| JButtonOperator |
bt |
push() |
| JCheckBoxMenuItemOperator |
cbmi |
|
| JCheckBoxOperator |
cb |
push() |
| JColorChooserOperator |
cch |
|
| JComboBoxOperator |
cbo |
selectItem() |
| JComponentOperator |
- |
|
| JDialogOperator |
- |
|
| JEditorPaneOperator |
txt |
typeText() |
| JFileChooserOperator |
fch |
|
| JFrameOperator |
- |
|
| JInternalFrameOperator |
- |
|
| JLabelOperator |
lbl |
|
| JListOperator |
lst |
|
| JMenuBarOperator |
menu |
|
| JMenuItemOperator |
mi |
|
| JMenuOperator |
mn |
|
| JPasswordFieldOperator |
txt |
typeText() |
| JPopupMenuOperator |
pop |
|
| JProgressBarOperator |
prb |
|
| JRadioButtonMenuItemOperator |
rbmi |
|
| JRadioButtonOperator |
rb |
push() |
| JScrollBarOperator |
scb |
|
| JScrollPaneOperator |
scp |
|
| JSliderOperator |
sli |
|
| JSplitPaneOperator |
spp |
|
| JTabbedPaneOperator |
tbp |
|
| JTableOperator |
tbl |
|
| JTextAreaOperator |
txt |
typeText() |
| JTextComponentOperator |
txt |
typeText() |
| JTextFieldOperator |
txt |
typeText() |
| JTextPaneOperator |
txt |
typeText() |
| JToggleButtonOperator |
tb |
push() |
| JTreeOperator |
tree |
|
| LabelOperator |
lbl |
|
| ListOperator |
lst |
|
| Operator |
- |
|
| ScrollPaneOperator |
scp |
|
| ScrollbarOperator |
scb |
|
| TextAreaOperator |
txt |
|
| TextComponentOperator |
txt |
|
| TextFieldOperator |
txt |
|
| WindowOperator |
- |
|