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