Writing your first Widget
The Basics
A widget is a flex module that gets run inside a window in the Community Edition application.
The Webpage handles all interactions with the window such as moving, dragging, changing size, hiding/showing, and data transfer. You are responsible for handling the content inside the widget.
The plugin sandbox application (InfininteTestApplication.mxml) displays widgets inside a movable/resizable window. This window is not present when deployed in Infinit.e and is only for testing your widget at different window sizes.
A widget must implement the com.ikanow.infinit.e.widget.library.widget.IWidget interface which is included in the infinit.e.widget.library.swc library.
The following functions are implemented:
- onInit(IWidgetContext)
- onReceiveNewQuery()
- onReceiveNewFilter()
- onParentResize(Number,Number)
The first 3 functions are for data transfer while the last is used for resizing the content you create.
If you are using the provided Eclipse Plugin then these methods will already be stubbed out for you with sample code.
Overview of Functions to Implement
The IWidget interface requires you to implement a number of methods, which will be called from the framework.
The following sections describe how to set up each of these methods.
Note that all these methods are stubbed out to do nothing by the plug-in. Therefore, only required functionality needs to be implemented. (However, all the functions must be present for the module to compile).
Mandatory
- onInit(context:IWidgetContext) - This function is called when your widget has completed loading into the Framework. The current data context object is passed to the widget which you will want to store locally. This IWidgetContext object contains methods to access the current result sets from the query, filter, and other associated information. This method has been stubbed out if you are using the provided Eclipse Plugin and no further action needs to be taken.
- onReceiveNewQuery() - This function is called by the framework when new data has been loaded into the IWidgetContext object. This is your notice that the dataset has changed and you can display new content in your widget. Some sample code has been stubbed out in the Eclipse Plugin that shows how you can get the query result set from the context object, passed to you on the onInit() function). Then that data can be iterated over to access various fields in the document set. An example is explained below on using query or filter data. TODO
- onRecieveNewFilter() - This function is similar to the onReceiveNewQuery function except it is called when a filter has been put on the query data set (even if this filter was applied by this widget). It can be used similarly to the onReceiveNewQuery function and an example can be seen below. TODO link to example.
Optional
- onParentResize(newH:Number,newW:Number) - This function is used to resize data in your widget. When the parent window the widget is housed in gets resized, it will let the module inside know that it has changed width and height, so you can change your data accordingly. If you are using the Eclipse Plugin this method has stubbed out an example for you that will set this module's width and height to the incoming width and height so the parent and internal module will stay the same size. Conversely, you may not want to do this if you are looking to scale the data in your window so it is always shown or resized in another manner.
- onSaveWidgetOptions():Object - This function is called periodically by the framework. The user can return an arbitrary JSON object (which is equivalent in ActionScript to an anonymous object, eg "var json:Object = { 'test': 'string', 'numbers': [ 1, 2, 3] };") which is stored by the framework and returned when the object is re-opened. This allows developers to save the widget state (note the size and position on the framework canvas is stored by default), such as level of zoom, which graphs are displayed, etc.
- onLoadWidgetOptions(widgetOptions:WidgetSaveObject) - This function restores the widget state saved by the above callback. Any JSON object stored using onSaveWidgetOptions will be returned here (this can include a community save object).
Advanced
- supportedExportFormats() - This function lets developers return an array collection of strings that appears in the widget's context menu. If one of these is selected by the user, onGenerateExportData (see below) is called back with the "format" parameter set to whichever string was selected.
- onGenerateExportData(filename:String, format:String):ByteArray - This function enables widget developers to generate arbitrary output (normally based on the visualization in the widget, eg KML for a map widget, RDFs for a link analysis widget, etc).
- onGeneratePDF(printPDF:PDF, title:String):PDF - This function is a special case of the above data export, allowing use of the AlivePDF library to create PDFs. If this function is left stubbed out then a bitmap snapshot of the widget will be generated instead. Override the function in order to support searchable text in the PDF, different formats etc.
Functionality Provided by the Framework Context
In addition to the above callbacks, the WidgetContext object provided to the developer by onInit allows a rich set of active operations to be performed, ranging from simple retrieval of documents to complex interactions such as modifying the framework's query builder or even performing local queries.
Future documentation will cover these capabilities in more detail, in the meantime:
- The API documentation is maintained here.
- The sections below provide examples on using this API in order to perform common tasks.
- The source code of the demonstration widgets can be obtained as a source of further examples.
Examples
Displaying Data Received from a Query or Filter
Here we will step through an example of displaying the titles and entities of a query to show how you can display the data your widget receives. We will create two lists in our widget: the left list will show the document titles, and the right list will show all the entities in the resulting dataset.
First, create a group to hold the two lists so they will align appropriately:
<s:HGroup width="100%" height="100%"> <s:List id="titleList" width="50%" height="100%" dataProvider="{titleArrayList}" /> <s:List id="entityList" width="50%" height="100%" dataProvider="{entityArrayList}" /> </s:HGroup>
Insert this text just below the Module tag in your sampleWidget. Also, create the two ArrayCollections we are using as dataProviders for the list inside the <fx:Script> block:
import mx.collections.ArrayCollection; [Bindable] private var titleArrayList:ArrayCollection = new ArrayCollection(); [Bindable] private var entityArrayList:ArrayCollection = new ArrayCollection();
Now we just need to load some data into these ArrayCollections and the lists will populate so we can show some data. Let's look at the onReceiveNewQuery function.
This function is going to get called every time a query is ran so we know to display new data. Let's first clear our ArrayCollections of any old data, and insert this code block at the beginning of the onReceiveNewQuery function:
titleArrayList.removeAll(); entityArrayList.removeAll();
Next we will loop through the result set we get from the IWidgetContext object we saved in the OnInit function and add the document's titles to our ArrayCollection. At the same time we will loop through the document entities adding them to our entity ArrayCollection so we can show their names. Also, add this code below the code we just added in the previous step:
var queryResults:ArrayCollection = _context.getQuery_AllResults().getTopDocuments(); for each (var doc:Object in queryResults ) { titleArrayList.addItem(doc.title); for each ( var entity:Object in doc.entities ) { entityArrayList.addItem(entity.disambiguated_name); } }
Now we can run our example, login, and do a sample query. We should have data displayed in our lists, including the following: all the results titles, and every entity in all the resulting documents.
Aggregated Entity Information Example
An alternative that only shows the aggregated entity information might look like:
var queryResults:ArrayCollection = _context.getQuery_AllResults().getEntities(); for each (var ent:Object in queryResults ) { entityArrayList.addItem(entity.disambiguous_name); }
For more details about the different views of the data provided, see the IWidgetContext class.
You can download the full code example here or see the widget code below.
Widget Header
The IWidget framework comes with a built in header that is attached to all widgets when initializing. At a minimum, the header will have the current title of the widget. If you are using the plugin, this can be updated by setting the title on your widget in the InfiniteTestApplication.mxml. Any options you want visible to the users at all times can be added to your widget in a section labeled: componants:headerContent
<components:headerContent> <!-- Put components to be displayed in the header here --> </components:headerContent>
The widget library comes complete with some components styled to fit in with the widget theme. A list of all available can be seen in the package com.ikanow.infinit.e.widget.library.components.*. If you have been using the widget plugin then a sample WidgetToggleButton is already created for you.
Common suggestions for header items include the following:
- Current graph types such as a toggle between pie or bar charts
- A drop down for current map display types such as Terrain, Satellite, or 2d Map
- A slider on how many nodes to show on a graph at once.
More Advanced Operations
Filtering the Data Visible by Widgets
Suppose you want to see quickly only those documents containing a specific set of entities, but don't want to make a whole new query. For example, you have a "Document Browser" widget open (like the first example above), and also an "aggregated event" widget (like the second example) and you only want to see all documents in the "document browser" containing the first entity listed.
In the "aggregated event" widget in some callback (eg. click on canvas), you would simply write some code like this:
var entitiesToFilter:Set = new HashSet(); entitiesToFilter.add(_context.getQuery_AllResults().getEntities().getItemAt(0)); _context.filterByEntities(FilterDataSetEnum.FILTER_GLOBAL_DATA, entitiesToFilter, EntityMatchTypeEnum.ANY, IncludeEntitiesEnum.INCLUDE_ALL_ENTITIES); // (Enums mean: (1) filter starting with all data, (2) apply OR to multiple entities in set, and (3) leave all entities in the resulting document set, not just those in the filter set)
After the final "filterByEntities" call, all active widgets have their "onReceiveNewFilter" callback invoked. This can be used analogously to the onReceiveNewQuery example shown above:
public function onReceiveNewFilter():void { var queryResults:ArrayCollection = _context.getQuery_FilteredResults().getTopDocuments(); for each (var doc:Object in queryResults) { titleArrayList.addItem(doc.title); for each ( var entity:Object in doc.entities ) { entityArrayList.addItem(entity.disambiguous_name); } } }
Note the filtering applies to all widgets, including the one making the call.
A visual example of widget filtering is available here.
Saving the Widget State Across Sessions
By default, when a widget is closed it loses all of its state (for example, in a "document search results" type widget, you might have a drop-down list specifying the number of documents to show per page). The exception to this is the location and size of the widget in the framework canvas.
The onSaveWidgetOptions and onLoadWidgetOptions provide an easy-to-use capability to store any desired state across sessions (ie closing and then re-opening a widget).
For example, assume a flex "ComboBox" object defined in the MXML, with id="documentsPerPage" (with options "5", "10", "20" and "50", from a bound array "_documentsPerPage"). Then the following code fragment would save this option for the logged-in user:
public function onSaveWidgetOptions():Object { var json:Object = { documentsPerPage: documentsPerPage.selectedItem.label }; return json; } public function onLoadWidgetOptions(save:WidgetSaveObject):void { if (null != save && null != save.userSave) { var option:String = save.userSave.documentsPerPage; if (null != option) { for (var i:int = 0; i < _documentsPerPage.length; ++i) { if (option == _documentsPerPage[i]) { documentsPerPage.selectedIndex = i; break; } } } } }
Notes:
- onSaveWidgetOptions is called periodically by the framework (so shouldn't block)
- onLoadWidgetOptions is called after the widget's initialization is complete.
- As can be seen by the code fragments above, the "Object" passed to/from the callbacks represents a JSON object.
- See the Generic Widget Functionality page for a distinction between userSaves and communitySaves.
Adding a Query Term to the Builder
This is a somewhat more advanced use of the IWidgetContext API, and requires some familiarity with the JSON query API.
Performing a local query
This is also a somewhat more advanced use of the IWidgetContext API requiring some familiarity with the JSON query API.
Code annex
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright 2012, The Infinit.e Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <components:WidgetModule xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:components="com.ikanow.infinit.e.widget.library.components.*" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" implements="com.ikanow.infinit.e.widget.library.widget.IWidget" creationComplete="{ try { onWidgetCreationComplete(); } catch (e:Error) { } dispatchEvent(new Event('Done Loading')); }"> <fx:Style source="/com/ikanow/infinit/e/assets/styles/infiniteStyles.css" /> <fx:Style> @namespace s "library://ns.adobe.com/flex/spark"; @namespace mx "library://ns.adobe.com/flex/mx"; /* If you need to override a style in our stylesheet, or add another style that we did not support you can do so here, an example has been commented out Please see documentation about over-riding MX component styles to display fonts*/ /* mx|Text { font-family: infiniteNonCFFFont; } */ </fx:Style> <fx:Script> <![CDATA[ import com.ikanow.infinit.e.widget.library.framework.WidgetSaveObject; import com.ikanow.infinit.e.widget.library.widget.IWidget; import com.ikanow.infinit.e.widget.library.widget.IWidgetContext; import mx.collections.ArrayCollection; import org.alivepdf.pdf.PDF; private var _context:IWidgetContext; private var titleArray:ArrayCollection = new ArrayCollection(); /** * Allow users to export the widget contents in the specified format * @format filename: the filename+path to which the data will be written (in case it needs to be embedded) * @param format: the format from the "supportedFormats" call * * @returns a ByteArray containing the data to output */ public function onGenerateExportData( filename:String, format:String ):ByteArray { return null; } /** * This function gets called when the user clicks to output * data to a PDF. Return null if custom PDF generation is * not desired. * * @return a new alivePdf Page containing the converted data */ public function onGeneratePDF( printPDF:PDF, title:String ):PDF { return null; } /** * IWidget interface to receive data object (IWidgetContext). * Store the iwidgetcontext so we can receieve data later. */ public function onInit( context:IWidgetContext ):void { _context = context; } /** * If a save object has been saved from 'onSaveWidgetOptions' then * when the app gets reloaded the last save string * will be passed to this function. * * @param widgetOptions the last save object or null if there was none */ public function onLoadWidgetOptions( widgetOptions:WidgetSaveObject ):void { //TODO } /** * function to rescale the component when the parent container is being resized * * @param newHeight The new height the component needs to be set to * @param newWidth The new width the component needs to be set to */ public function onParentResize( newHeight:Number, newWidth:Number ):void { this.height = newHeight; this.width = newWidth; } /** * IWidget interface that fires when a new filter is done (including from ourself) * We can access the data fromt he filter by using our * iwidgetcontext object _context.getQuery_FilteredResults().getTopDocuments(); */ public function onReceiveNewFilter():void { //get filtered logic here //_context.getQuery_FilteredResults().getTopDocuments(); } /** * IWidget interface that fires when a new query is done. * We can access the data from the query by using our * iwidgetcontext object context.getQuery_TopResults().getTopDocuments(); */ public function onReceiveNewQuery():void { //get documents var queryResults:ArrayCollection = _context.getQuery_TopResults().getTopDocuments(); //set the labels of these docs and use as dataprovider for list for each ( var doc:Object in queryResults ) doc.label = doc.title; titlesList.dataProvider = queryResults; } /** * This function gets called when the workspace is being saved. * return null if no save object is needed. * * @return an object this widget can use to reload state */ public function onSaveWidgetOptions():Object { return null; } /** * @returns A list of supported formats, displayed in a context menu in the format * "Export <string>" - these are called with "generateExportData" * Note this doesn't cover the "built-in" Alive PDF export. * However if the developer specifies PDF and generatePdf() returns non-null then this will be used. */ public function supportedExportFormats():ArrayCollection { return null; } /** * The callback handler for clicking the sample button in the header of the app. * * @param event The mouse event when clicking the button. **/ protected function sampleButton_clickHandler( event:MouseEvent ):void { //perform some action when header button is clicked } /** * Method fired when module is done loading. Sends * message to parent letting it know that module is * ready to receive data. */ private function onWidgetCreationComplete():void { } ]]> </fx:Script> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations> <!-- If you would like this widget to be styled similar to the other infinite widgets you may place items in the headerContent section shown below and they will be drawn at the top of the widget. If you want to use similar looking buttons explore the com.ikanow.infinit.e.widget.library.components.* items looking for components prefixed with Widget*. Other components may be added to the header as well. --> <components:headerContent> <s:HGroup gap="-3"> <!-- Ignore Filter Toggle Button --> <components:WidgetToggleButton id="sampleButton" label="Sample Header Button" toolTip="This is the tooltip for a header button" click="sampleButton_clickHandler(event)" /> </s:HGroup> </components:headerContent> <s:VGroup width="100%" height="100%" horizontalAlign="center" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5" verticalAlign="middle"> <s:Label text="Sample Module" fontSize="30" /> <s:List id="titlesList" width="100%" height="100%" /> </s:VGroup> </components:WidgetModule>