Ubuntu logo

Developer

Unity 8 Scope Example: Openclipart

Scope Example: Openclipart

Here you can get started writing Unity 8 Scopes. We provide an example that searches openclipart.com for images with a query string and implements categorised search results and a set of preview widgets allocated to two different layouts for use in different display situations (narrow and wide).

We provide the source code and discuss all the key points below.

Note: Your development system does not need to run Unity 8 on the desktop to run the example scope.

Before getting started

You may want to read the Unity 8 Scopes Guide first. This provides an overview of key concepts you need to understand when developing scopes, including the flow of events, the scope query and its generated results, the preview and its widgets and layouts, and more.

Install the Ubuntu SDK

See Ubuntu SDK Tutorials for a quick tutorial. You can now create Unity 8 C++ scope projects in the SDK with the Unity Scope Template project type.

Get the example scope source branch

Ensure the bzr package is installed.

$ sudo apt-get install bzr

If you are new to bzr, you need to configure bzr:

$ bzr whoami "John Doe <john.doe@gmail.com>"

You also need a launchpad.net account configured. For this page for help.

Get the example branch, as follows:

$ bzr branch lp:ubuntu-sdk-tutorials

Move into the example scope directory:

$ cd ubuntu-sdk-tutorials/scopes/openclipart

Open the example in the SDK

Tip: See Scope Development Procedures for important concepts and common SDK procedures used when developing scopes.

Launch the Ubuntu SDK and open the example directory as a project with File > Open File or Project and  select the ubuntu-sdk-tutorials/scopes/scope-openclipart/CMakeLists.txt file. (You can also use the Ctrl + O keyboard shortcut.)

You can now build the project with Ctrl + B. This creates a sibling directory: build-scope-openclipart-Desktop-Default/

And, you can run it from terminal by moving into the build directory and executing:

$ unity-scope-tool src/openclipart-scope.ini

Key C++ source files

Let’s start with a review of the project C++ files.

src/openclipart-scope.cpp

This defines a class of type unity::scopes::ScopeBase that provides the entry point API the client uses to interact with the scope.

  • It implements start, stop and run methods. Many scopes can leave these unmodified, and this example does as well.
  • It also implements two key methods: search and preview. These methods often do not need to be modified and they are not modified in this example. However, they call critical methods that do need to be implemented in every scope, as discussed below.

Note: You may find it useful to check out the ScopeBase class declaration (its API) in the corresponding header file: openclipart-scope.h. The header file is a great way to understand C++ classes because their API is declared without any additional implementation code, making it easy to understand.

Tip: Check out the Unity 8 Scope API reference docs at developer.ubuntu.com/api/scopes/sdk-14.04/

src/openclipart-query.cpp

This file defines a class of type unity::scopes::SearchQueryBase.

This class generates search results from a query string a client provides and returns them as a reply to the client:

  • Receives the query string from the client
  • Receives a reply object from the client
  • Generates search results (this part is scope specific)
  • Creates search result categories (for example with different layouts – grid/carousel)
  • Combines each search result with its category (creating CategorisedResult objects)
  • Pushes categorised results into the reply object for display by the client

Much of the coding work is done in the run method, as shown below.

Check out the SearchQueryBase class declaration (its API) in the corresponding header file: openclipart-query.h.

src/openclipart-preview.cpp

This key file defines a class of type unity::scopes::PreviewQueryBase.

This class defines the widgets and layouts used for each search result during the preview phase. It:

  • Defines the widgets used in previews
  • Maps widget fields to data fields in each result
  • Defines layouts with different numbers of columns – known only by the client at display time
  • Assigns widgets to columns for each layout
  • Receives a reply object and pushes the widgets and layouts onto it for use by the client

Much of the coding work is done in the run method, as shown below.

Check out the SearchPreviewBase class declaration (its API) in the corresponding header file: openclipart-preview.h.

For list of Preview Widgets and documentation, see this page.

Let’s drill into our example scope and detail some of the code, starting with the query.

The Query: search results generation phase

This phase uses openclipart-query.cpp, which creates a SearchQueryBase class and implements its key run method.

Note: The code that generates search results from a web API is scope-specific and the approach generally changes depending on the data source. Openclipart provides a JSON API, so there is JSON parsing code in this example. Others data source APIs are common, for example RSS/XML. Custom code appropriate to the scope’s data source API is generally needed.

Query string

The query string is a passed (from the client) as an argument to the SearchQueryBase class constructor. You can see this in the header file:

OpenclipartQuery(std::string const& query);

(OpenclipartQuery is our example’s class of type SearchQueryBase.)

query_ is also declared as a private variable:

std::string query_;

The constructor itself simply receives the query string from the client and initializes query_ with its value:

OpenclipartQuery::OpenclipartQuery(std::string const& query) :

   query_(query) { }

Tip: At the bottom of the file in the run method a default query is set to “cat”. This is useful during development to ensure results display on launch with the unity-scope-tool.

The base of openclipart’s web API  URI is also set:

const QString BASE_URI = "http://openclipart.org/search/json/?query=%1";

Search results are generated by opening the BASE_URI with %1 substituted with the query string. You can open the following in a browser to understand the JSON data structure returned by the openclipart API:

http://openclipart.org/search/json/?query=cat

Most of the work is done in the query’s run method. Let’s review the key points.

Creating and registering CategoryRenderers

Two CategoryRenders are created from JSON objects.  These are created as raw strings. The JSON objects have two fields of immediate interest: template and components. Here is an example:

std::string CR_GRID = R"(
   {
       "schema-version" : 1,
       "template" : {
           "category-layout" : "grid",
           "card-size": "small"
       },
       "components" : {
           "title" : "title",
           "art" : {
               "field": "art",
               "aspect-ratio": 1.6,
               "fill-mode": "fit"
           }
       }
   }
)";

The template field allows you to select defined layout properties for the results view. Here a grid layout is used, along with small cards.

The components field allows you to include pre-defined fields for each result. Here we add title and art (which itself includes two pre-defined fields).

As we show below, these defined fields (like title) have methods you use to set the data for each that is used at results display time. Naturally, if you do not include a field, it is not displayed.

Tip: Check out CategoryRenderer class docs.

A CategoryRenderer is created for both JSON obects, and then registered on the reply object:

CategoryRenderer rdrGrid(CR_GRID);
CategoryRenderer rdrCarousel(CR_CAROUSEL);
auto catGrid = reply->register_category("openclipartgrid", "OpenclipartGrid", "", rdrGrid);
auto catCar = reply->register_category("openclipartcarousel", "OpenclipartCarousel", "", rdrCarousel);

Now, they are available for use. Now, each generated search result needs to be assigned to one of these categories, as we see next.

Note: You can also use the default CategoryRenderer instead of creating one from a JSON string.

Generating search results

The example contains code that enables querying the openclipart URI and receiving and parsing the returned JSON object. We skip a detailed look at that code and continue with the for loop that iterates over the results:

for(const auto &result : results) {
   [...]
}

The example branch simply switches between the two CategoryRenders (&catGrid and &catCar). This is done here to demonstrate two categories in our results. But, a real scope would assign results to categories meaningfully based on data analysis.

Each result requires a CategorisedResult object. This takes a CategoryRenderer during construction. The CategorisedResult object is used to populate actual search result data into. It is pushed into the reply object later.

CategorisedResult catres((*cat));

Tip: cat is a pointer to the current CategoryRenderer (alternating between grid and layout views).

Next, variables are created for each field of interest in the search result:

auto title = resJ["title"].toString();
auto uploader = resJ["uploader"].toString();
auto artist = "Artist: " + resJ["drawn_by"].toString();
auto uri = resJ["detail_link"].toString();
auto image = resJ["svg"].toObject()["png_thumb"].toString();
auto desc = resJ["description"].toString();

These variables are used to populate our catres object:

catres.set_uri(uri.toStdString());
catres.set_dnd_uri(uri.toStdString());
catres.set_title(title.toStdString());
catres.set_art(image.toStdString());
catres["description"] = Variant(desc.toStdString());
catres["artist"] = Variant(artist.toStdString());

The first four lines use standard CategorisedResult set_ methods to put the values into well-known  catres fields, some of which are declared in our JSON CategoryRenders (like title and art) and some of which are implicit (like uri).

However, the last two lines populate data into fields that are not used in the results view and are not declared in our CategoryRenders. These custom fields are used in the preview. To use them in the preview, they are added to the CategorisedResult object. (Later, we look at how to map them to preview widget fields.)

These custom fields are set using key/value notation with the result that each CategorisedResult object has a description key (whose value is specific to each search result) and an artist key (whose value is also result specific.

Our CategorisedResult obejct now has all of its data fields set and its CategoryRenderer set. It is pushed into the reply object, thus making it available for display in the client:

if (!reply->push(catres)) {
   break; // false from push() means search was cancelled
}

Here is how this results phase looks when launched in the unity-scope-tool:

results-grid-carousel.png

The Preview

The preview needs to generate widgets and connect their fields to the data fields in the CategorisedResult.

It also should generate layouts to handle different display environments. The idea is that only the client knows the layout context. The client thinks of the display context it in terms of the number columns available. The scope defines which columns to put widgets into for layouts with different numbers of columns.

First, let’s take a look at widgets.

Preview Widgets

There is a set of predefined Preview Widgets. Each has a type field you use to create them. Each type of widget also has additional fields that vary by widget type.

You can see the the list of Preview Widget types and the fields they offer here.

This example uses four types of Preview Widgets:

  • header: has a title and a subtitle field
  • image: has a source field used to retrieve the art from
  • text: has a text field
  • actions: used to provide button text “Open” and the URI opened when the user clicks the preview

Here’s how our example creates a header widget named w_header:

PreviewWidget w_header("headerId", "header");
  • The first parameter is an arbitrary ID. We use these IDs to assign the widget to different layouts, as shown later.
  • The second parameter is the Preview Widget type, one of the set of pre-defined types.

After widget creation, the widget fields are populated with data from the CategorisedResult being processed by the client. Our w_header widget’s standard fields: title and subtitle are populated.

Two methods are available to put data into widget fields:

  • add_attribute_value(FIELD, VALUE): You can use this method to simply populate data you have on hand into the widget field
  • add_attribute_mapping(FIELD, CR_FIELD): Use this method to populate data from the CategorisedResult being processed into the widget field.

In our example, widget data is derived from the current CategorisedResult, and so add_attribute_mapping is used.

First, let’s map the w_header widget’s title field (the first parameter) to the title field in the current CategorisedResult (the second parameter):

w_header.add_attribute_mapping("title", "title");

The next example is a little more interesting because we populate a widget field from a CategorisedResult field that is not part of the CategoryRenderer. The field is artist. We added the artist key and value directly to our CategorisedResult for each result previously. So this example shows how to display data in your preview even when the data is not displayed in results phase and is custom to the scope:

w_header.add_attribute_mapping("subtitle", "artist");

Looking back at the query, where the CategorisedResults were created, we see again how the artist data was made available to the CategorisedResult:

catres["artist"] = Variant(artist.toStdString());

As a result of that, each CategorisedResult has an “artist” field populated from the search result. And in this preview phase, we push that artist data into the w_header widget’s predefined subtitle field.

We now need to push our widgets to the client. This is done with the reply object.

But first the widgets have to be combined into a PreviewWidgetList:

PreviewWidgetList widgets({w_header, w_art, w_info, w_actions});

And now they can be pushed to the client with the reply object:

reply->push(widgets);

The widgets are created , populated, and pushed.  But, the client also needs to know where to put the widgets, and even how to arrange the widgets nicely in different contexts, for example a narrow screen and a wide screen, so let’s take a look at layouts.

Generating Layouts

Our example defines two layouts: one with a single column, and one with two columns. These are declared like this:

ColumnLayout layout1col(1), layout2col(2);

Tip: Check out ColumnLayout docs here.

We do not need to know exactly how the client uses these. But the general expectation is that a single-column layout is appropriate for narrow-screen situations (like portrait mode) and a two-column layout may be appropriate for wide screen situations (like landscape mode).

Now, we need to define where our four widgets are going to go in each of these layouts.

Naturally, in a single-column layout, all widgets have to go into that single column:

Layout1col.add_column({"headerId", "artId", "infoId", "actionsId"});

In the two-column layout, we decide to add the header and the image to the first column and the text (infoId) and the text and the actions to the second column:

layout2col.add_column({"artId", "headerId"});
layout2col.add_column({"infoId", "actionsId"});

Now, we need to register the layouts into the reply object, as follows:

reply->register_layout({layout1col, layout2col});

Here is how a preview looks in the unity-scope-tools:

preview-withtool.png

Summary

  • We have seen how to make a scope that queries a web API
  • Query results are put into two categories, each of which has a different renderer (one for grid, one for carousel)
  • The client displays search results sorted by type, and honors the layout (grid or carousel)
  • For the preview phase, four predefined widget types were used
  • Two layouts were created in which widgets were allocated differently, one for a single column at display time, one for two columns available at display time
  • Some custom data appropriate only for this scope (artist) is displayed in the preview