Custom Widgets
A widget is a modular component in the GUI that enables users to access information or perform a specific function. A user or an enterprise can incorporate custom widgets into their workspaces for various purposes including the following:
- Custom OneClick filters
- Order Entry Panels
- Highly customized views of market data, positions, or any other data within Deephaven
- Creating a custom widget requires knowledge of Java and Swing programming techniques.
The process of creating widgets is illustrated below:
- First, a query is used to create the widget and specify the data to be included.
- When a widget is called by a user in the Deephaven console, an instruction is sent from the client to the server. The server "deflates" the widget and its state and sends them to a named instance on the client.
- When the client receives the deflated widget, it is "inflated" to its original form. The client then uses the inflated widget to install the GUI "component". Upon completion of this process, the resulting widget is displayed in the Deephaven console.
- When the source table on the server updates with live data, the widget can also be updated through listeners on the specified data.
Note
Custom Widgets are currently only supported in Deephaven Classic.
Creating a Widget
A custom widget must extend the LiveWidget class and implement two methods:
deflate()
getComponent()
Objects must be deflated on the server side before they can be shipped to the client:
Inflatable<OBJECT_TYPE> deflate(ExportedObjectClient client);
The deflate
method creates an object to hold all the information needed to create the GUI component. The object is of type Inflatable<LiveWidget>
, which allows the client to inflate your widget after it has been deflated and shipped to the client.
After the LiveWidget has been inflated on the client, the GUI calls the getComponent
method to create a Swing component, which is then installed within the Deephaven Console:
JComponent getComponent(AsyncPanel dataPanel, String viewId, String title, Object irisWidgetSupportObject, Logger log);
Example
In the following example, we are going to create a widget to demonstrate the general steps involved in creating your own widget. This example widget, called "LabelCustomWidget" will display a value from the last row of a table in the Deephaven console, as shown in the image below.
After creating and installing your class, you can run a query in your Deephaven console that imports the Java package and uses the LabelCustomWidget.
Step 1: Create the Java Class for the Widget
To create a widget, you must first create a Java class that implements the com.illumon.iris.db.tables.utils.LiveWidget
interface. This interface allows the Deephaven client and server to interact appropriately to create the GUI component on the client.
public class LabelCustomWidget implements LiveWidget<LabelCustomWidget> {
// table that will provide data to display on the widget
private final Table baseTable;
// name of the column that will be displayed
private final String columnName;
// construct a new custom widget
public LabelCustomWidget(final Table baseTable, final String columnName) {
// basic error checking to make sure the table contains the data
// we want to display
if(!baseTable.hasColumns(columnName)) {
throw new IllegalArgumentException("Base table does not contain column " + columnName);
}
// lastBy() creates a table with only the last value of each column
// view(...) reduces table only to specified columns
// preemptiveUpdatesTable(...) specifies how frequently the table
// will update in the GUI
this.baseTable = baseTable.lastBy()
.view(columnName)
.preemptiveUpdatesTable(1000L);
this.columnName = columnName;
}
Step 2: Implement WidgetState
The WidgetState
is used to help inflate and deflate the widget to ship it from server to client.
public static class WidgetState implements Inflatable<LabelCustomWidget> {
// Inflatable<Table> must be used instead of Table
// in order to ship a handle to the table rather
// than the whole table
private final Inflatable<Table> table;
private final String columnName;
public WidgetState(final Inflatable<Table> table, final String columnName) {
this.table = table;
this.columnName = columnName;
}
// returns widget state from deflated format into one usable by client
@Override
public LabelCustomWidget inflate(QueryProcessorConnection queryProcessorConnection) {
return new LabelCustomWidget(table.inflate(queryProcessorConnection), columnName);
}
}
Step 3: Implement deflate
All Custom Widgets must implement the deflate method, which will return a WidgetState
object (that was implemented as an inner class in Step 2. Deflation allows the client to maintain handles to tables on the server rather than copying the entire data set over, which would defeat the purpose of the client-server architecture.
// deflates state of the widget as required to send over network
// once received by the client, it will then be inflated to utilize said state
@Override
public Inflatable<LabelCustomWidget> deflate(ExportedObjectClient client) {
return new WidgetState(baseTable.deflate(client), columnName);
}
Step 4: Implement getComponent
All Custom Widgets must implement getComponent()
. This will create the Swing JComponent
that will display in the GUI.
// the getComponent() method creates the Swing JComponent
@Override
public JComponent getComponent(AsyncPanel dataPanel, String viewId, Object view, String title, Object irisWidgetSupportObject, Logger log) {
// returns a new instance of the Swing GUI widget
return new LastRowLabel(baseTable, columnName);
}
Step 5: Define the Component to be Displayed in the Console
The next snippet creates the actual widget that will be displayed in the GUI. This simple example uses a JLabel
(a display area for a short text string), but more complicated widgets will want to extend JPanel. For more information about creating GUI widgets, please refer to Creating a GUI With JFC/Swing.
// implementation of the actual widget that will be displayed in the console
public class LastRowLabel extends JLabel {
private final Table baseTable;
// reference to listener is required so it is not garbage collected
// and therefore stopped from working
private final InstrumentedListener instrumentedListener;
public LastRowLabel(Table table, final String columnName) {
// this allows the widget to receive table data from the server
this.baseTable = table.subscribeToPreemptiveUpdates();
// listener to update Swing widget when table changes
instrumentedListener = new InstrumentedListenerAdapter("LastRowLabel", (DynamicTable) baseTable, false) {
@Override
public void onUpdate(Index added, Index removed, Index modified) {
final long lastIndex = baseTable.getIndex().lastKey();
final Object value = baseTable.getColumn(columnName).get(lastIndex);
// GUI components must be updated on the Swing thread
SwingUtilities.invokeLater(() -> setText(value.toString()));
}
};
// subscribe to dynamic table changes
((DynamicTable) baseTable).listenForUpdates(instrumentedListener, true);
// set the Swing widget font style
setHorizontalAlignment(SwingConstants.CENTER);
setFont(getFont().deriveFont(50f));
setForeground(Color.RED);
}
}
Step 6: Install the Custom Widget
Compile the Java code into .class files, archive them into a .jar file, and install the .jar file in the customer directory; details are shown in Installing Custom Classes. Note that the client update service must be restarted and the client's Swing console must be updated, so that the new custom widget class will be available to client as well as the server.
Step 7: Open the Widget in the Console
To use the widget in the console, run the following query after installing your class on the server:
t = db.timeTable("00:00:01")
import com.illumon.iris.db.tables.utils.LabelCustomWidget
myWidget = new LabelCustomWidget(t, "Timestamp")
t = db.timeTable("00:00:01")
LabelCustomWidget = jpy.get_type("com.illumon.iris.db.tables.utils.LabelCustomWidget")
myWidget = LabelCustomWidget(t, "Timestamp")
You must import the widget with its fully qualified name. See the full example code below.
Full Example Code
import com.fishlib.io.logger.Logger;
import com.illumon.iris.db.tables.Table;
import com.illumon.iris.db.tables.remote.ExportedObjectClient;
import com.illumon.iris.db.tables.remote.Inflatable;
import com.illumon.iris.db.tables.remotequery.QueryProcessorConnection;
import com.illumon.iris.db.v2.DynamicTable;
import com.illumon.iris.db.v2.InstrumentedListener;
import com.illumon.iris.db.v2.InstrumentedListenerAdapter;
import com.illumon.iris.db.v2.utils.Index;
import com.illumon.iris.gui.widget.AsyncPanel;
import javax.swing.*;
import java.awt.*;
public class LabelCustomWidget implements LiveWidget<LabelCustomWidget> {
// table that will provide data to display on the widget
private final Table baseTable;
// name of the column that will be displayed
private final String columnName;
// construct a new custom widget
public LabelCustomWidget(final Table baseTable, final String columnName) {
// basic error checking to make sure the table contains the data
// we want to display
if(!baseTable.hasColumns(columnName)) {
throw new IllegalArgumentException("Base table does not contain column " + columnName);
}
// lastBy() creates a table with only the last value of each column
// view(...) reduces table only to specified columns
// preemptiveUpdatesTable(...) specifies how frequently the table
// will update in the GUI
this.baseTable = baseTable.lastBy()
.view(columnName)
.preemptiveUpdatesTable(1000L);
this.columnName = columnName;
}
public static class WidgetState implements Inflatable<LabelCustomWidget> {
// Inflatable<Table> must be used instead of Table
// in order to ship a handle to the table rather
// than the whole table
private final Inflatable<Table> table;
private final String columnName;
public WidgetState(final Inflatable<Table> table, final String columnName) {
this.table = table;
this.columnName = columnName;
}
// returns widget state from deflated format into one usable by client
@Override
public LabelCustomWidget inflate(QueryProcessorConnection queryProcessorConnection) {
return new LabelCustomWidget(table.inflate(queryProcessorConnection), columnName);
}
}
// deflates state of the widget as required to send over network
// once received by the client, it will then be inflated to utilize said state
@Override
public Inflatable<LabelCustomWidget> deflate(ExportedObjectClient client) {
return new WidgetState(baseTable.deflate(client), columnName);
}
// the getComponent() method creates the Swing JComponent
@Override
public JComponent getComponent(AsyncPanel dataPanel, String viewId, Object view, String title, Object irisWidgetSupportObject, Logger log) {
// returns a new instance of the Swing GUI widget
return new LastRowLabel(baseTable, columnName);
}
// implementation of the actual widget that will be displayed in the console
public class LastRowLabel extends JLabel {
private final Table baseTable;
// reference to listener is required so it is not garbage collected
// and therefore stopped from working
private final InstrumentedListener instrumentedListener;
public LastRowLabel(Table table, final String columnName) {
// this allows the widget to receive table data from the server
this.baseTable = table.subscribeToPreemptiveUpdates();
// listener to update Swing widget when table changes
instrumentedListener = new InstrumentedListenerAdapter("LastRowLabel", (DynamicTable) baseTable, false) {
@Override
public void onUpdate(Index added, Index removed, Index modified) {
final long lastIndex = baseTable.getIndex().lastKey();
final Object value = baseTable.getColumn(columnName).get(lastIndex);
// GUI components must be updated on the Swing thread
SwingUtilities.invokeLater(() -> setText(value.toString()));
}
};
// subscribe to dynamic table changes
((DynamicTable) baseTable).listenForUpdates(instrumentedListener, true);
// set the Swing widget font style
setHorizontalAlignment(SwingConstants.CENTER);
setFont(getFont().deriveFont(50f));
setForeground(Color.RED);
}
}
t = db.timeTable("00:00:01")
LabelCustomWidget = jpy.get_type("com.illumon.iris.db.tables.utils.LabelCustomWidget")
myWidget = LabelCustomWidget(t, "Timestamp")
t = db.timeTable("00:00:01")
import com.illumon.iris.db.tables.utils.LabelCustomWidget
myWidget = new LabelCustomWidget(t, "Timestamp")
Setting Access Controls for a Custom Widget
To limit which users can view your widget from Persistent Queries, your widget must implement the LiveWidgetVisibilityProvider
interface and override the getValidGroups()
method. This tells Deephaven which Access Control (ACL) groups may view the widget.
For example, we could modify the "LabelCustomWidget" to have a String[] validGroups
variable and a corresponding getter and setter:
public class LabelCustomWidget implements LiveWidget<LabelCustomWidget>, LiveWidgetVisibilityProvider {
private final Table baseTable;
private final String columnName;
private String[] validGroups;
...
public void setValidGroups(final String... validGroups) {
this.validGroups = validGroups;
}
public String[] getValidGroups() {
return validGroups;
}
...
A user can set the access controls in the persistent query that generates the widget. An example query to do that follows:
t = db.timeTable("00:00:01")
import com.illumon.iris.db.tables.utils.LabelCustomWidget
myWidget = new LabelCustomWidget(t, "Timestamp")
myWidget.setValidGroups("User1", "User2", "Group1")
t = db.timeTable("00:00:01")
LabelCustomWidget = jpy.get_type("com.illumon.iris.db.tables.utils.LabelCustomWidget")
myWidget = LabelCustomWidget(t, "Timestamp")
myWidget.setValidGroups("User1", "User2", "Group1")
When this query is executed in Deephaven, only User1, User2 and individuals included in Group1 are allowed to open and view the widget using the Show Widget button in the Deephaven console.
For another example, see Setting Permissions for Viewing Plots.