4.3.3.2. ZK Dialogs

The second solution utilizes the UI framework that is used by Docmenta to create the web interface. It requires some knowledge of the ZK framework. In short, ZK is an Open Source framework for creating web interfaces in Java. In principle you can create a web interface in ZK without having to know the details of HTML or JavaScript. Following example gives a short introduction on how to use the ZK framework. More information can be found on the ZK homepage.
As an example, we'll add a configuration dialog to the plug-in, which has been created in Section 4.3.2, “Adding menu items”. The dialog shall allow to configure the text to be displayed in the "Hello World" message box. In more detail: the dialog shall contain a text box where the user can enter some text, and a "Save" as well as a "Cancel" button.
In ZK the user interface is defined in an XML-based language named ZUML (ZK User Interface Markup Language). It is similar to HTML, but has some more advanced features for defining interactive user interfaces. Following ZUML code defines our configuration dialog:
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
<window id="my_plugin_config_dialog"
title="${labels.my_plugin.config_dialog_title}"
border="normal"
width="460px"
contentStyle="padding:10px;"
visible="false"
sizable="false"
closable="true"
onClose="self.visible = false; event.stopPropagation();"
apply="myexample.MyDialogComposer" >
<caption>
<toolbarbutton id="config_help_btn"
label="${labels.my_plugin.help_btn}" />
</caption>
 
<vbox spacing="6px" width="100%" align="stretch">
<hbox align="center">
<label>${labels.my_plugin.config_message_label}:</label>
<textbox id="config_textbox" maxlength="100" hflex="1" />
</hbox>
<hbox spacing="4px">
<button id="config_save_btn"
label="${labels.my_plugin.save_btn}"
image="img/save.gif"
hflex="1" />
<button id="config_cancel_btn"
label="${labels.my_plugin.cancel_btn}"
image="img/cancel.gif"
hflex="1" />
</hbox>
</vbox>
</window>

Listing 4.3.13. my_config_dialog.zul

Following a short description of the elements. For further information, please consult the ZUML documentation on http://www.zkoss.org.
The window element
In ZUML, popup windows can be created using the window element. In our example, the window gets the ID "my_plugin_config_dialog". Note that the IDs of windows have to be unique within the Docmenta application. To avoid naming conflicts it is therefore a good idea to include the plug-in ID as prefix. Furthermore the code above provides a title for the dialog via the title attribute. As the title may be language dependent, the title is loaded as property named "my_plugin.config_dialog_title" from the locale.properties file. To read a property value from locale.properties the notation ${labels.name} can be used in ZUML (where name is the name of the property to be retrieved).
The attribute border="normal" just sets the default border to be used for the window. The width attribute defines the width of the dialog. Note that no height is specified here, as the height shall be automatically determined from the content. The contentStyle attribute allows to set some custom CSS to be applied to the content. In our example, a padding of 10 pixels is defined, i.e. 10 pixels of space will be inserted between the border of the window and the content of the window.
The attribute visible can be used to show or hide a window. Because our dialog shall only pop up if user clicks on the configuration button of our plug-in, the dialog is set to be initially hidden (i.e. visible="false"). The attribute sizable="false" disables resizing of the window by the user.
The attribute closable="true" has the effect that a close-button is shown in the top-right corner of the window:

Figure 4.3.11. Close button in the title-bar of the dialog

In our example, the close-button shall just close the dialog without saving the user-input, i.e. it shall have the same effect as clicking the "Cancel" button. The attribute onClose allows to define the action to be performed when the user clicks on the close-button. In ZUML a special script language is available that allows to include scripts within ZUML. In our example, the script "self.visible = false;" hides the window, if user clicks on the close-button. Furthermore, the statement "event.stopPropagation();" prevents the "onClose" event to be further propagated to the default event handler. This is required in our example, because the default event handler completely destroys the window instance. But we want to reuse the window instance in case the user opens and closes the dialog multiple times within a session.
Finally, the attribute apply defines a custom Java class that implements the dynamic part of the dialog. The code for the class myexample.MyDialogComposer is shown below.
The caption element
A window can optionaly contain a caption element. The caption element allows to add buttons to the title bar of a window. In our example, a toolbarbutton element with ID "config_help_btn" is added to the window's title bar. A toolbarbutton is like a normal button, but requires less space, because it is rendered without the border of a normal button. The button shall provide a link to the online-help of the plug-in.

Figure 4.3.12. Help button in the title-bar of the dialog

As for the window title, the button's label is retrieved as property named my_plugin.help_btn from the locale.properties file.
Defining the layout using vbox and hbox
The elements that follow the caption element define the content of the window. In our example, a vbox element is the root of the content. A vbox element aligns its child-elements vertically, whereas a hbox element aligns its child-elements horizontally. In our example, the vbox element contains two hbox elements. Therefore, our layout consists of two rows, each row containing elements that are horizontally aligned.
The spacing attribute defines the space to be inserted between child-elements. The attribute width="100%" assures that the vbox element fills the complete width of the dialog (otherwise the width of the vbox would be determined from the preferred width of the child-elements). The attribute align="stretch" has the effect, that the width of the child-elements is stretched to the complete width of the vbox.
The attribute align="center" of the hbox element has the effect, that in case the child-elements do not have the same height, then the child-elements are centered vertically.
Defining the input fields using label, textbox and button
The first row of our layout contains a label and a textbox element. In ZUML the label element is used to display static text. In our example, the text to be displayed is again retrieved from the locale.properties file, by using the notation ${labels.name}, where name is the name of the property to be retrieved.
The textbox element represents a input field where the user can enter text. The optional attribute maxlength defines the maximum number of characters that the user is allowed to enter. The attribute hflex defines how the horizontal space is partitioned between the child-elements. If an element has no hflex attribute, then the size of the element is determined by its content. However, if the parent-element has more horizontal space available than the sum of the preferred widths of the child-elements, then the extra space is divided up between all child-elements. The hflex attribute allows to assign a weight to the child-element, that determines how much of the extra space shall be assigned to the child-element. In our example, because the label element has no hflex attribute, and the textbox element has the attribute hflex="1", the complete extra space will be assigned to the textbox.
The second row contains the "Okay" and "Cancel" buttons. The labels for both buttons are again retrieved from the locale.properties file by using the ${labels.name} notation. Furthermore, the attribute image defines images to be displayed in front of the labels. Note that the image URLs "img/save.gif" and "img/cancel.gif" reference images that are included in the Docmenta installation by default. Therefore these images do not have to be included in the plug-in package.
The width of the buttons is defined by the hflex="1" attribute. Because both buttons get the same weight, any extra space will be divided up between both buttons equally.
Assuming that following lines have been added to locale.properties:

my_plugin.config_dialog_title = My Configuration
my_plugin.help_btn = Help
my_plugin.save_btn = Save
my_plugin.cancel_btn = Cancel
my_plugin.config_message_label = Enter your text:

Listing 4.3.14. Lines added to locale.properties

the resulting dialog should be similar to following:

Figure 4.3.13. Configuration dialog example

But before we can test the ZUML code, two more things have to be done: we have to implement the composer class myexample.MyDialogComposer, and we have to include the dialog in our plug-in implementation. 
Implementing a Composer
In the ZUML code above, a Java class named myexample.MyDialogComposer is referenced by the window element. In ZK a so-called composer initializes components. In our example, we use a composer to initialize the dialog. A composer has to implement the org.zkoss.zk.ui.util.Composer interface. ZK already provides an implementation of this interface, named org.zkoss.zk.ui.select.SelectorComposer. This class allows using Java annotations, to wire the elements in a ZUML page with the Java code. Following listing shows the implementation of our composer that extends from the SelectorComposer class:
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
package myexample;
 
import org.zkoss.util.resource.Labels;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.*;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zul.*;
 
public class MyDialogComposer extends SelectorComposer<Component> {
 
private MyMenuPlugin plugin;
@Wire Window my_plugin_config_dialog;
@Wire Textbox config_textbox;
 
public void openDialog(MyMenuPlugin plugin)
{
this.plugin = plugin;
config_textbox.setValue(plugin.getMessage());
my_plugin_config_dialog.doHighlighted();
}
@Listen("onClick = #config_save_btn")
public void onOkayClick() throws Exception
{
plugin.setMessage(config_textbox.getValue());
my_plugin_config_dialog.setVisible(false);
}
 
@Listen("onClick = #config_cancel_btn")
public void onCancelClick()
{
my_plugin_config_dialog.setVisible(false);
}
@Listen("onClick = #config_help_btn")
public void onHelpClick()
{
String help_url = Labels.getLabel("my_plugin.help_url");
String js = "window.open('" + help_url + "', '_blank', " +
"'width=850,height=600,resizable=yes,scrollbars=yes," +
"location=yes,menubar=yes,status=yes');";
Clients.evalJavaScript(js);
}
}

Listing 4.3.15. MyDialogComposer.java

The class extends from SelectorComposer to allow usage of the @Wire and @Listen annotations. Note that also the classes from the org.zkoss.zk.ui.select.annotation package have to be imported. For every type of element in ZUML, a equally named Java class exists in the org.zkoss.zul package. For example, for the <textbox /> element in ZUML, a corresponding class org.zkoss.zul.Textbox exists.
In our example, we have to access the dialog-window and the textbox from within Java. Therefore the two member fields my_plugin_config_dialog and config_textbox exist. As you can see, the name of the member fields is equal to the id attribute that has been assigned in the ZUML file (my_config_dialog.zul). The @Wire annotation initializes a Java member field with the object that represents the element in the ZUML page, i.e. the element that has an id attribute value equal to the name of the member field.
 In the ZUML page of our dialog, the attribute visible="false" has been added to the <window /> element. That means, our dialog is closed by default. Therefore we have to provide a method that allows us to open the dialog. As you might guess, this is done by the method openDialog().  The org.zkoss.zul.Window element provides several methods to make a window visible. You could just call the method setVisible(true). However, our window shall be displayed as a modal dialog, i.e. the Docmenta main window shall not be accessible until our plug-in dialog is closed again. To achieve this, the method doHighlighted() is called instead of setVisible(true). Please consult the ZK documentation for more information.
Before the dialog is opened, the textbox needs to be initialized with the current configuration setting. To retrieve the currently configured message, our plug-in will be extended with a method named getMessage(). Furthermore, the class org.zkoss.zul.Textbox provides a method named setValue() to initialize the input field.
The next three methods, onOkayClick(), onCancelClick() and onHelpClick() are event handlers. That means, these methods are called if certain events occur. Which kind of event causes a method call is defined by the @Listen annotation. For example, the annotation @Listen("onClick = #config_save_btn"), causes a method call, in case an "onClick" event occurs for the element with id attribute "config_save_btn". Simply said, the onOkayClick() method is called, if the user clicks on the "Okay" button, the onCancelClick() method is called, if user clicks on the "Cancel" button and the onHelpClick() method is called, if user clicks on the "Help" button (in the window's title bar).
If the user clicks on the "Okay" button, then the entered text has to be stored as the new configuration setting. This is done by the statement plugin.setMessage(config_textbox.getValue()). Note that the method setMessage() still needs to be added to our plug-in (see below). The expression config_textbox.getValue() returns the text that is currently contained in the input field of the dialog. The statement my_plugin_config_dialog.setVisible(false) finally closes the dialog.
The method onCancelClick() just closes the dialog (without changing the configuration setting).
The method onHelpClick() opens a help page, by sending a "window.open(...)" JavaScript command to the client browser. Note that the URL of the help page is retrieved as property "my_plugin.help_url" from the locale.properties file (i.e. you have to add a corresponding line to this file and include the referenced help file in the plug-in package). 
Extending the Plug-in class
Now, we have to add the getMessage() and setMessage() methods to our plug-in class, i.e. the methods that are used in MyDialogComposer.java to read and write the configured message. Furthermore, we have to overwrite the onLoad() method, to load the configured message on server start-up, and the onShowConfigDialog() method, to open the configuration dialog. Following listing shows the extended version of the plug-in. The line-numbers of the added and changed lines are highlighted:
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
package myexample;
 
import java.io.*;
import org.docma.plugin.*;
import org.docma.plugin.web.*;
import org.zkoss.zul.Window;
 
public class MyMenuPlugin extends DefaultWebPlugin
implements UIListener
{
static final String MY_ITEM_ID = "my_plugin_menu_item";
 
private MyDialogComposer dialog_composer;
private File config_file;
private String message = "";
public String getMessage()
{
return this.message;
}
public void setMessage(String msg) throws IOException
{
this.message = msg;
FileWriter out = new FileWriter(this.config_file);
out.write(msg);
out.close();
}
@Override
public void onLoad(PluginContext ctx) throws Exception
{
File dir = ctx.getPluginDirectory();
this.config_file = new File(dir, "config.txt");
if (this.config_file.exists()) {
BufferedReader in = new BufferedReader(
new FileReader(this.config_file));
this.message = in.readLine();
in.close();
}
}
@Override
public void onShowConfigDialog(WebPluginContext ctx,
WebUserSession sess)
{
this.dialog_composer.openDialog(this);
}
 
@Override
public void onInitMainWindow(WebPluginContext ctx,
WebUserSession sess)
{
String item_label = sess.getLabel("my_plugin.show_hello");
String icon_url = "plugins/my_plugin/images/my_icon.png";
sess.addMenuItem(ctx, "treemenu", MY_ITEM_ID, item_label,
icon_url, "treemenuExtra", true);
sess.setUIListener(ctx, this);
String windowId =
sess.addDialog("plugins/my_plugin/my_config_dialog.zul");
Window dialog = (Window) sess.getDialog(windowId);
this.dialog_composer =
(MyDialogComposer) dialog.getAttribute("$composer");
}
 
@Override
public void onEvent(UIEvent evt)
{
WebUserSession sess = evt.getSession();
String targetId = evt.getTargetId();
if (MY_ITEM_ID.equals(targetId) && evt.isClick()) {
sess.showMessage(this.message);
}
}
 
}

Listing 4.3.16. MyMenuPlugin.java (configuration dialog extensions)

A member field named message has been added to the plug-in, which stores the configured message. The method getMessage() just returns the value of this field. The method setMessage() is a little bit more complicated, because it also has to write the message to a file. Otherwise the configuration setting would be lost on server restart.
The path of the file that is used to store the configured message is given in the member field config_file. This field is initialized in the plug-in's onLoad() method (see Section 4.3.1, “Lifecycle methods”). The configuration file is named "config.txt" and is stored in the directory returned by the expression ctx.getPluginDirectory(). In Docmenta all installed plug-ins are stored in the sub-directory named "plugins" in the document-store directory. For each installed plug-in a separate sub-directory exists, which is called the "Plug-in directory". This directory contains the installed plug-in package. If a new version of Docmenta is installed, then all previously installed plug-ins are restored from this directory. This directory can also be used by the plug-in, to store any kind of data, e.g. configuration settings. A plug-in can retrieve the path to this directory from the org.docma.plugin.PluginContext instance by calling the method getPluginDirectory().
Besides initializing the member field config_file, the method onLoad() also sets the member field message to the content of the configuration file. This way, on server restart the configured message does not get lost.
The implementation of the lifecycle method onShowConfigDialog() is quite simple: it just calls the method openDialog() of the composer instance (see MyDialogComposer.java above). The composer instance is stored in the member field dialog_composer, which is initialized in the lifecycle method onInitMainWindow().
The method onInitMainWindow() is similar to the previous version of the example plug-in. The only differences are some added lines for the configuration dialog. First, our configuration dialog is added to the web-interface of the current session by calling the method addDialog() and passing the path to the dialog's ZUML file. The method returns the id of the root element in the passed ZUML file (in our example this is "my_plugin_config_dialog"). Then, the expression sess.getDialog(windowId) retrieves the instance of the root element, i.e. in our example an instance of org.zkoss.zul.Window. Finally, the expression dialog.getAttribute("$composer") retrieves the composer instance that has been declared for the window in the ZUML file (via the apply attribute). The method getAttribute() is provided by ZK for all components. By convention, the attribute named "$composer" stores the composer that is assigned to the component.
The method onEvent() is identical to the previous version of the plug-in, except that the message to be displayed is no longer retrieved from locale.properties, but from the member field message.
The plugin.properties file
Before we can package and install the plug-in, we have to update the plugin.properties file as follows:

id=my_plugin
version=1.0
plugin_class=myexample.MyMenuPlugin
required_app_version = 1.9
load_type = next_startup
config_dialog = true
keep_files = config.txt

Listing 4.3.17.

As you can see, two lines have been added: The line "config_dialog = true" has the effect that after the plug-in has been loaded, a configuration button will be displayed, as shown in following screenshot:

Figure 4.3.14. Plug-in configuration button

If the user clicks the configuration button, the plug-in's onShowConfigDialog() method is called.
The line "keep_files = config.txt" has the effect, that in case a newer version of the plug-in is installed, the file named config.txt in the Plug-in directory is not deleted, i.e. the configuration settings of the old version is reused for the new version. This avoids that the user has to re-enter all the configuration settings in case the plug-in is upgraded. The value of the keep_files property can be a list of relative file paths. All the file paths have to be given relative to the Plug-in directory.
We can now create a zip package and install the plugin as described in Chapter 4.2, Creating a plug-in package. The content of the package should be as follows: 

Figure 4.3.15. Package structure of the dialog plug-in

Testing the plug-in
After installation of the plug-in, the plug-in control should display a "Configure" button (see Figure 4.3.14, “Plug-in configuration button”). Clicking this button opens our configuration dialog:

Figure 4.3.16. The plug-in configuration dialog

To update the plug-in configuration, enter a text and click the "Save" button. The entered text should now be stored in the config.txt file in the plug-in directory. After the plug-in has been configured, clicking the "Hello World" menu item displays the configured text.
Be aware that in our example only one plug-in configuration exists for all users. That means, if one user (with administrator rights) changes the plug-in configuration, all users are affected. However, if a plug-in needs to store user-specific settings, this can be realized as shown in Section 4.3.4, “Adding a tab”.