Advanced UI Customization

Level: Advanced Module: Plugin Development 12 min read Lesson 31 of 47

Overview

  • What you’ll learn:
    • How iDempiere uses the ZK Ajax framework and how to extend existing windows and components programmatically
    • How to create custom dashboard gadgets, Info Windows, and custom editor components for specialized data entry
    • How to customize themes, toolbars, and keyboard panels, and deploy all UI modifications as OSGi plugins
  • Prerequisites: Lesson 15 — Building Your First Plugin, Lesson 4 — Navigating the User Interface
  • Estimated reading time: 24 minutes

Introduction

iDempiere’s web interface is built on the ZK Framework, a server-centric Ajax framework that provides rich desktop-like interactions in the browser. While the Application Dictionary handles most UI generation automatically — creating windows, tabs, and fields from metadata — there are scenarios where you need to go beyond what the dictionary can express. Custom dashboard gadgets, specialized editor components, theme modifications, and programmatic window extensions all require direct interaction with the ZK layer.

This lesson teaches you how to work with iDempiere’s UI framework at the code level. You will learn how ZK works, how to extend existing windows, how to build custom components, and how to package everything as deployable plugins.

ZK Ajax Framework Overview

Understanding ZK’s architecture is essential before customizing iDempiere’s UI.

Server-Centric Model

Unlike JavaScript-heavy frameworks (React, Angular), ZK follows a server-centric model. UI components exist as Java objects on the server. When a user interacts with the browser (clicking, typing, scrolling), ZK’s client engine sends Ajax requests to the server, which updates the component tree and sends back incremental UI updates. Developers write Java code, and ZK handles all JavaScript, HTML, and CSS generation.

// This Java code creates a button and handles its click — no JavaScript needed
Button button = new Button("Process Order");
button.addEventListener(Events.ON_CLICK, event -> {
    // This runs on the server when the user clicks
    processOrder();
    Clients.showNotification("Order processed successfully");
});

ZUL Templates

ZK also supports ZUL, an XML-based markup language for defining UI layouts declaratively:

<?xml version="1.0" encoding="UTF-8"?>
<window title="Custom Panel" border="normal" width="400px">
    <vlayout>
        <label value="Product Search" style="font-weight:bold"/>
        <hlayout>
            <textbox id="searchField" width="250px" placeholder="Enter product name"/>
            <button id="searchBtn" label="Search"/>
        </hlayout>
        <listbox id="resultList" height="300px" emptyMessage="No results">
            <listhead>
                <listheader label="Code" width="100px"/>
                <listheader label="Name" width="200px"/>
                <listheader label="Price" width="100px"/>
            </listhead>
        </listbox>
    </vlayout>
</window>

iDempiere uses a combination of programmatic Java code and ZUL templates for its UI. Custom forms and dashboard gadgets often use ZUL templates for layout, with Java classes providing the logic.

Key ZK Components Used by iDempiere

  • Window / Panel: Top-level containers for content areas
  • Tabbox / Tab / Tabpanel: Tabbed interface containers (used for AD tabs)
  • Grid / Rows / Row: Table-like layout for form fields
  • Listbox / Listitem: Scrollable list/grid for record display
  • Textbox, Intbox, Datebox, Combobox: Input components for different data types
  • Borderlayout / Vlayout / Hlayout: Layout containers for organizing components

Extending Existing Windows Programmatically

The most common UI customization is adding behavior to existing windows without modifying iDempiere core code. Model validators and event handlers allow you to intercept window events and modify behavior.

Using IFormController for Custom Window Logic

You can create a form controller that attaches custom logic to Application Dictionary windows:

import org.adempiere.webui.adwindow.ADWindow;
import org.adempiere.webui.event.ActionEvent;
import org.adempiere.webui.event.ActionListener;

public class CustomOrderWindowHandler implements ActionListener {

    private ADWindow adWindow;

    public void initialize(ADWindow window) {
        this.adWindow = window;
        // Listen for toolbar button events
        window.getADWindowContent().getToolbar()
            .addListener(this);
    }

    @Override
    public void actionPerformed(ActionEvent event) {
        String action = event.getActionCommand();
        if ("Process".equals(action)) {
            // Add custom validation before document processing
            if (!validateCustomRules()) {
                Dialog.warn(adWindow.getADWindowContent().getWindowNo(),
                    "Custom validation failed");
                event.setConsumed(true); // Prevent default processing
            }
        }
    }

    private boolean validateCustomRules() {
        // Custom validation logic
        return true;
    }
}

Adding Custom Buttons to Windows

You can add custom buttons to existing window toolbars through event handlers:

import org.adempiere.base.event.IEventTopics;
import org.adempiere.webui.adwindow.ADWindow;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

@Component(
    property = {
        EventConstants.EVENT_TOPIC + "=" + IEventTopics.WINDOW_AFTER_OPEN
    }
)
public class WindowCustomizer implements EventHandler {

    @Override
    public void handleEvent(Event event) {
        ADWindow adWindow = (ADWindow) event.getProperty("ADWindow");
        int windowId = adWindow.getAD_Window_ID();

        // Only customize specific windows (e.g., Sales Order window)
        if (windowId == 143) {
            ToolBarButton customBtn = new ToolBarButton("CustomAction");
            customBtn.setLabel("Verify Inventory");
            customBtn.setTooltiptext("Check inventory availability for all lines");
            customBtn.addEventListener(Events.ON_CLICK, e -> {
                verifyInventory(adWindow);
            });
            adWindow.getADWindowContent().getToolbar().appendChild(customBtn);
        }
    }
}

Info Windows (AD_InfoWindow) Customization

Info Windows are the lookup/search dialogs that appear when users click the magnifying glass icon on a field. iDempiere allows you to customize these through the Application Dictionary or programmatically.

Application Dictionary Customization

Navigate to System Admin > General Rules > System Rules > Info Window to configure Info Windows:

  • Info Columns: Define which columns appear in the search results grid, their display order, and widths.
  • Search Criteria: Define which fields appear as search filters in the Info Window header.
  • SQL Validation: Add WHERE clause conditions to restrict which records appear (e.g., show only active customers).
  • Custom SQL: Override the default query with custom SQL for complex data requirements.

Programmatic Info Window Extension

For more complex customizations, extend the Info Window classes:

import org.adempiere.webui.info.InfoWindow;

public class CustomProductInfoWindow extends InfoWindow {

    @Override
    protected void initComponents() {
        super.initComponents();
        // Add a custom filter component
        Checkbox inStockOnly = new Checkbox();
        inStockOnly.setLabel("In Stock Only");
        inStockOnly.addEventListener(Events.ON_CHECK, e -> {
            refreshData();
        });
        addSearchComponent(inStockOnly);
    }

    @Override
    protected String buildWhereClause() {
        String where = super.buildWhereClause();
        // Add custom filter condition
        if (inStockOnly.isChecked()) {
            where += " AND QtyOnHand > 0";
        }
        return where;
    }
}

Custom ZK Components (Extending WEditor)

iDempiere uses WEditor subclasses to render different field types in windows. The editor system maps Application Dictionary display types to specific ZK components:

  • WStringEditor for text fields
  • WNumberEditor for numeric fields
  • WDateEditor for date fields
  • WTableDirEditor for table-direct lookups
  • WSearchEditor for search fields

Creating a Custom Editor

If you need a specialized input component — for example, a color picker, a barcode scanner input, or a map coordinate selector — create a custom WEditor:

import org.adempiere.webui.editor.WEditor;
import org.zkoss.zul.*;

public class WColorEditor extends WEditor {

    private Textbox colorInput;
    private Div colorPreview;

    public WColorEditor(GridField gridField) {
        super(new Hlayout(), gridField);
        initComponents();
    }

    private void initComponents() {
        Hlayout layout = (Hlayout) getComponent();

        colorInput = new Textbox();
        colorInput.setMaxlength(7);  // #RRGGBB
        colorInput.setWidth("80px");
        colorInput.setPlaceholder("#000000");
        colorInput.addEventListener(Events.ON_CHANGE, event -> {
            String color = colorInput.getValue();
            colorPreview.setStyle("background-color:" + color +
                ";width:24px;height:24px;border:1px solid #ccc;display:inline-block");
            // Notify the framework of the value change
            ValueChangeEvent vce = new ValueChangeEvent(
                this, getColumnName(), getOldValue(), color);
            fireValueChange(vce);
        });

        colorPreview = new Div();
        colorPreview.setStyle("width:24px;height:24px;border:1px solid #ccc;display:inline-block");

        layout.appendChild(colorInput);
        layout.appendChild(colorPreview);
    }

    @Override
    public String getDisplay() {
        return colorInput.getValue();
    }

    @Override
    public Object getValue() {
        return colorInput.getValue();
    }

    @Override
    public void setValue(Object value) {
        if (value == null) {
            colorInput.setValue("");
        } else {
            colorInput.setValue(value.toString());
            colorPreview.setStyle("background-color:" + value +
                ";width:24px;height:24px;border:1px solid #ccc;display:inline-block");
        }
    }
}

Registering a Custom Editor

Register your custom editor to handle a specific display type by implementing an editor factory:

import org.adempiere.webui.factory.IEditorFactory;

@Component(service = IEditorFactory.class)
public class CustomEditorFactory implements IEditorFactory {

    @Override
    public WEditor getEditor(GridTab gridTab, GridField gridField, boolean tableEditor) {
        // Use custom editor for fields with a specific display type or column name
        if ("Color".equals(gridField.getColumnName()) ||
            gridField.getDisplayType() == MY_CUSTOM_DISPLAY_TYPE) {
            return new WColorEditor(gridField);
        }
        return null; // Return null to let the default factory handle it
    }
}

Theme Customization

iDempiere’s visual appearance can be customized through CSS and ZUL template overrides without modifying core code.

CSS Customization

Create a theme plugin that overrides the default styles. The theme is loaded through a ZK theme provider:

/* custom-theme.css */

/* Override the main header color */
.z-north .desktop-header-left {
    background-color: #1a5276;
}

/* Customize the menu tree */
.z-west .z-tree {
    font-size: 13px;
}

/* Highlight required fields */
.mandatory-decorator-text .z-textbox,
.mandatory-decorator-text .z-intbox,
.mandatory-decorator-text .z-decimalbox {
    border-left: 3px solid #e74c3c;
}

/* Custom styling for specific windows */
.ad-window-content .z-grid {
    border: 1px solid #ddd;
    border-radius: 4px;
}

/* Dashboard gadget styling */
.dashboard-widget {
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 12px;
}

/* Responsive adjustments for smaller screens */
@media (max-width: 1024px) {
    .z-west {
        width: 200px !important;
    }
}

Registering a Theme Plugin

Create a theme provider to inject your custom CSS:

import org.adempiere.webui.theme.ITheme;
import org.osgi.service.component.annotations.Component;

@Component(service = ITheme.class, property = { "theme.id=custom-theme" })
public class CustomTheme implements ITheme {

    @Override
    public String[] getStyleSheets() {
        return new String[] { "~./css/custom-theme.css" };
    }

    @Override
    public String[] getScripts() {
        return null; // No custom JavaScript
    }
}

Place your CSS files in the plugin’s web/css/ directory and register the web resource path in your MANIFEST.MF.

Dashboard Gadgets

Dashboard gadgets are widgets displayed on the iDempiere home screen. Creating custom gadgets is one of the most impactful UI customizations because the dashboard is the first thing users see.

The DashboardPanel Interface

All dashboard gadgets implement the DashboardPanel interface or extend a base class:

import org.adempiere.webui.dashboard.DashboardPanel;
import org.zkoss.zul.*;

public class RecentOrdersGadget extends DashboardPanel {

    private Listbox orderList;

    public RecentOrdersGadget() {
        super();
        initComponents();
        loadData();
    }

    private void initComponents() {
        this.setTitle("Recent Sales Orders");

        Vlayout layout = new Vlayout();
        layout.setStyle("padding: 8px;");

        orderList = new Listbox();
        orderList.setMold("paging");
        orderList.setPageSize(10);
        orderList.setEmptyMessage("No recent orders");

        Listhead head = new Listhead();
        head.appendChild(new Listheader("Document No", null, "120px"));
        head.appendChild(new Listheader("Customer", null, "200px"));
        head.appendChild(new Listheader("Date", null, "100px"));
        head.appendChild(new Listheader("Total", null, "100px"));
        orderList.appendChild(head);

        layout.appendChild(orderList);
        this.appendChild(layout);
    }

    private void loadData() {
        Properties ctx = Env.getCtx();
        String sql = "SELECT o.DocumentNo, bp.Name, o.DateOrdered, o.GrandTotal " +
                     "FROM C_Order o " +
                     "JOIN C_BPartner bp ON o.C_BPartner_ID = bp.C_BPartner_ID " +
                     "WHERE o.IsSOTrx='Y' AND o.AD_Client_ID=? " +
                     "ORDER BY o.DateOrdered DESC";

        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = DB.prepareStatement(sql, null);
            pstmt.setInt(1, Env.getAD_Client_ID(ctx));
            rs = pstmt.executeQuery();
            int count = 0;
            while (rs.next() && count++ < 10) {
                Listitem item = new Listitem();
                item.appendChild(new Listcell(rs.getString(1)));
                item.appendChild(new Listcell(rs.getString(2)));
                item.appendChild(new Listcell(rs.getTimestamp(3).toString()));
                item.appendChild(new Listcell(rs.getBigDecimal(4).toString()));
                orderList.appendChild(item);
            }
        } catch (Exception e) {
            log.log(Level.SEVERE, sql, e);
        } finally {
            DB.close(rs, pstmt);
        }
    }

    @Override
    public boolean isPooled() {
        return true; // Gadget can be pooled/reused across sessions
    }
}

Registering the Dashboard Gadget

Register your gadget in the Application Dictionary so users can add it to their dashboard:

  1. Navigate to System Admin > General Rules > System Rules > Dashboard Content.
  2. Create a new record with:
    • Name: Recent Sales Orders
    • ZUL File Path: Leave empty (for Java-based gadgets)
    • Class Name: com.example.dashboard.RecentOrdersGadget
    • Column No: 0 (left column), 1 (center), 2 (right)
    • Line No: Controls vertical position within the column
    • Is Show in Dashboard: Yes

Alternatively, register it via OSGi declarative services and a dashboard panel factory:

import org.adempiere.webui.factory.IDashboardGadgetFactory;

@Component(service = IDashboardGadgetFactory.class)
public class CustomGadgetFactory implements IDashboardGadgetFactory {

    @Override
    public DashboardPanel getGadget(String uri, Component parent) {
        if ("recent-orders".equals(uri)) {
            return new RecentOrdersGadget();
        }
        return null;
    }
}

Toolbar Customization

The window toolbar in iDempiere contains standard buttons for navigation, CRUD operations, and document processing. You can customize it by adding, removing, or modifying toolbar buttons.

Adding a Custom Toolbar Button

// Using the window event handler approach
@Override
public void handleEvent(Event event) {
    ADWindow adWindow = (ADWindow) event.getProperty("ADWindow");

    ToolBarButton exportBtn = new ToolBarButton();
    exportBtn.setImage(ThemeManager.getThemeResource("images/Export24.png"));
    exportBtn.setTooltiptext("Export to Excel");
    exportBtn.addEventListener(Events.ON_CLICK, e -> {
        exportCurrentView(adWindow);
    });

    // Add button after the standard toolbar buttons
    adWindow.getADWindowContent().getToolbar().appendChild(new Space());
    adWindow.getADWindowContent().getToolbar().appendChild(exportBtn);
}

Hiding Standard Buttons

To hide standard toolbar buttons for specific windows (e.g., hide the Delete button on read-only windows):

ToolBarButton deleteBtn = adWindow.getADWindowContent().getToolbar()
    .getButton("Delete");
if (deleteBtn != null) {
    deleteBtn.setVisible(false);
}

Window Event Handling

ZK’s event system allows you to intercept and respond to any user interaction. Common events you can handle in iDempiere windows:

// Listen for tab change events
adWindow.getADWindowContent().getADTab()
    .addEventListener(ADTabpanel.ON_ACTIVATE_EVENT, event -> {
        int tabIndex = adWindow.getADWindowContent().getADTab().getSelectedIndex();
        // Perform custom logic when a specific tab is activated
    });

// Listen for field value changes
gridField.addEventListener(Events.ON_CHANGE, event -> {
    Object newValue = gridField.getValue();
    // React to the field value change
});

// Listen for grid row selection
listbox.addEventListener(Events.ON_SELECT, event -> {
    Listitem selected = listbox.getSelectedItem();
    // Handle row selection
});

Form vs. Window Customization

Understanding when to use a custom form versus customizing an AD window is important for choosing the right approach:

Aspect AD Window Customization Custom Form
Data entry for AD tables Preferred — uses AD metadata Overkill for simple data entry
Complex UI layout Limited by AD structure Full control over layout
Multi-step wizard Not supported Ideal approach
Dashboard-like display Not applicable Good fit
Interactive processing Limited Full control
Maintenance effort Low (AD-driven) Higher (custom code)

Use AD window customization (model validators, callouts, event handlers) when you need to modify behavior of standard data entry windows. Use custom forms when you need a completely different UI paradigm, such as a multi-step wizard, a graphical process monitor, or an interactive dashboard.

Deploying UI Customizations as Plugins

All UI customizations should be packaged as OSGi plugins for clean deployment and maintainability:

  1. Create a plugin project with dependencies on org.adempiere.ui.zk and org.adempiere.base.
  2. Place ZUL templates in the plugin’s web/ directory, which is accessible via the ~./ URL prefix in ZK.
  3. Place CSS and images in web/css/ and web/images/ respectively.
  4. Register components using OSGi declarative services (@Component annotations).
  5. Test the plugin by deploying it to your development iDempiere instance and verifying that UI changes appear correctly.
  6. Export as a JAR for deployment to production via the P2 update mechanism.

Bundle your plugin’s MANIFEST.MF to export web resources:

Web-ContextPath: /webui
Bundle-ClassPath: .,
 web/

Summary

You now have a comprehensive understanding of iDempiere’s UI customization capabilities. From extending existing windows with event handlers and custom toolbar buttons, to creating entirely new dashboard gadgets and editor components, to restyling the entire application with custom themes — all deployed cleanly as OSGi plugins. The ZK framework provides the component model, iDempiere provides the extension points, and OSGi provides the deployment infrastructure. In the next lesson, you will learn how to create professional reports using JasperReports integrated with iDempiere.

You Missed