Define the UI via JSON and change it via function API

In RemoteSkin for Swing, UI widgets are grouped into widget sets and described in a JSON file. Widget sets can describe entire windows or dialogs, but they are also useful for smaller parts of the UI, e.g. parts of forms, context or dropdown menus, contents of tabbedPanes, etc.

Besides the pure definition of the UI, however, the manipulation of the widgets during use is also part of everyday life. This is how data is inserted, the status of widgets is changed and new elements are displayed. For these purposes we provide a function API.

For client-side events, lambdas can also be registered using the function API.

UI definition

The definition of the UI is done in a JSON file. This contains four sections with different functions.

Widget definitions can be reused very easily via copy & paste. Once you have an inventory of widget descriptions, assembling a new widget set is very easy. Recurring attribute combinations can be grouped together in templates, which speeds up the work a lot.

Anyone wondering why the widget set IDs and widget IDs contain special characters (@,#) and numbers: These can be found very easily in widget set descriptions and Java code. We use one number per widget set (here 200). The widget IDs get a '#' as prefix, the widget set IDs get an '@'. Of course, everyone can do this as desired.

Widget set

This section serves as a place for the attributes of the widget set itself. The main thing to find here is the widget set ID.

"WidgetSet": {
    "WidgetSetName": "@200_ChooseNode",
    "DebugBox": false
}

Widget definition

Widgets are defined in the "Widgets" section. Next to the widget ID, the attributes and their values are listed.

Internally, we mostly use the well-known MigLayout for the layout, as this has advantages over the well-known Swing layouts. Swing layouts can of course be used as well.

"Widgets": [
    {
      "id": "#200_Panel_Main",
      "widgetType": "Panel",
      "opaque": false,
      "MigLayout": {
        "layout": "noVisualPadding, fill, wrap 1",
        "column": "12[fill,grow]12",
        "row": "16[fill]5[fill,grow]5[fill]5[fill]10[fill]10"
      }
    },
    {
      "id": "#200_Label_Title",
      "widgetType": "Label",
      "text": "Choose a NODE:",
      "Font": {
        "name": "Helvetica bold",
        "style": "PLAIN",
        "size": 20
      },
      "foreground": "white"
    },
    {
      "id": "#200_ScrollPane",
      "widgetType": "ScrollPane",
      "Templates": "ListBorder"
    },
    {
      "id": "#200_List",
      "widgetType": "ListBox",
      "Templates": "ComponentBackground",
      "onMouseDoubleClick": true,
      "PopupOnRightClick": "@201_PopupNode",
      "noBorder": true,
      "listSelectionMode": "single"
    },
    {
      "id": "#200_Panel_Filter",
      "widgetType": "Panel",
      "opaque": false,
      "MigLayout": {
        "layout": "insets 0,fillX",
        "column": "[]10[fill, grow]",
        "row": "fill, grow"
      }
    },
    {
      "id": "#200_Label_Filter",
      "widgetType": "Label",
      "text": "Filter:",
      "foreground": "white",
      "labelFor": "#200_TextField_Filter",
      "mnemonic": "F"
    },
    {
      "id": "#200_TextField_Filter",
      "widgetType": "TextField",
      "Templates": "EditBorder,ComponentBackground",
      "wantChange": true
    },
    {
      "id": "#200_Panel_Buttons",
      "widgetType": "Panel",
      "opaque": false,
      "MigLayout": {
        "layout": "insets 0,fillX",
        "column": "",
        "row": "fill, grow"
      }
    },
    {
      "id": "#200_Button_OpenNodeInfo",
      "widgetType": "Button",
      "text": "Open Node Info",
      "mnemonic": "O",
      "constraints": "alignX trailing"
    }
]

Hierarchy

The hierarchy of widgets among themselves is mapped in this section. We decided to separate attribute definition and hierarchy because it seems much clearer to us.

"Hierarchy": {
    "#200_Panel_Main": [
      "#200_Label_Title",
      "#200_ScrollPane",
      "#200_Panel_Filter",
      "#200_Panel_Buttons",
      "#,widgetType=PlaceHolder,widgetSetName=@202_NodeDescription"
    ],
    "#200_ScrollPane": [
      "#200_List"
    ],
    "#200_Panel_Filter": [
      "#200_Label_Filter",
      "#200_TextField_Filter"
    ],
    "#200_Panel_Buttons": [
      "#200_Button_OpenNodeInfo"
    ]
}

Templates

Finally, recurring attribute descriptions are included in the template section. The templates are assigned to the widgets with the Templates attribute.

"Templates": {
    "ListBorder": {
      "compoundBorder": {
        "outerBorder": {
          "borderType": "matte",
          "top": 10,
          "left": 0,
          "right": 0,
          "bottom": 10,
          "color": "lightgray"
        },
        "innerBorder": {
          "borderType": "empty",
          "all": 8
        }
      }
    },
    "EditBorder": {
      "compoundBorder": {
        "outerBorder": {
          "borderType": "matte",
          "top": 0,
          "left": 10,
          "right": 10,
          "bottom": 0,
          "color": "lightgray"
        },
        "innerBorder": {
          "borderType": "empty",
          "all": 8
        }
      }
    },
    "ComponentBackground": {
      "background": "white"
    }
}

Function API

Widgets display data or states. These must be communicated to the widget before or during display. We use an API based on functions for this purpose.

Internally, the functions ensure that data is added to a message. Any number of functions of the API can be called one after the other. Finally, the message is sent to the client with a sendUpdate().

The following example shows a function that is called after mounting a new dialog. Here the dialog is filled with data, then the visible flag is set and sendUpdate() is called. The data is placed in the corresponding widgets of the client in the same order. Therefore, the visible flag is set at the end.

private void widgetSetMounted(@NotNull final CWidgetSetId aWidgetSetId)
{
    mApi.pack(m701Dialog);
    mApi.centerWindow(m701Dialog,
                      mParentWidget);
    mApi.setText(m701UserId,
                 mUser.getId());
    mApi.setText(m701RealName,
                 mUser.getRealName());
    mApi.setText(m701Email,
                 mUser.getEmail());
    mApi.setEnabled(m701UserId,
                    false);
    mApi.setVisible(m701Dialog,
                    true);
    sendUpdate();
}

Since the messages are sent in compressed form, the amount of data transported between client and server is very low.

Many API functions require event classes for transporting composite data. An event object is created and filled with related data, which is then added to the message using an API function.

private void onRemoveUser(@NotNull final CEventButtonPressedNotification aEvent)
{
    final CEventShowMessageDialog event = new CEventShowMessageDialog("Do you really want to remove user \"" + mUser.getId() + "?\"");
    event.setTitle("Remove User");
    event.setButtons(new String[]{
            "Okay",
            "Cancel"
    });
    event.setDefaultButton("Okay");
    event.setIcon(CEventIcon.fromObject("user.png"));
    mApi.showMessageDialog(m700ButtonDeleteUser,
                           event);
    mServerContext.sendUpdate();
}

Events

Events are also set via API. For this purpose there is an addListener() method that accepts lambdas, interfaces or private handlers. If the corresponding event occurs on the client side, the data of the event is transmitted to the owner target. This then leads to a call of the lambda.

private final CWidgetId m702ButtonOkay;

// in the constructor
m702ButtonOkay = CWidgetId.create(CWidgetSet_702.BUTTON_OKAY,
                                  mWidgetSetId702);
mApi.addListener(m702ButtonOkay,
                 (IButtonPressedListener) this::onButtonOkay);

// handler
private void onButtonOkay(@NotNull final CEventButtonPressedNotification aEvent) throws CException
{
    ...
    unmount();
}

The owner target

Each widget set has an owner. The owner is a target, i.e. an object that can receive messages in nyssr.net. The owner can work on any node in nyssr.net, and is identified by its target address. This is also the reason that the owners of different widget sets of the same program can run on different nodes. As a reminder, we have completely free messaging here. If I have an address of a target, I can send a message to the target. And a target that has the address of a RemoteSkin client can add UI elements there in the form of widget sets.