Lenses

Differences between revisions 19 and 20
Revision 19 as of 2011-09-09 18:01:15
Size: 16020
Editor: 87-194-53-170
Comment:
Revision 20 as of 2011-09-12 19:01:34
Size: 23838
Editor: c-76-109-5-30
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:

''''' This is a testing page for the new Lenses API docs, and might already be outdated when you read it!'''''
Line 4: Line 7:
''''' This is referring to the 11.04 Lenses, the 11.10 documentation will be uploaded shortly '''''


= Unity Lenses Architecture =

/!\ Lenses used to be known as Places. When referring to code and APIs we still use the term Places, but the user-visible name is Lenses. The Places name will be retired in 11.10.
Line 13: Line 9:
This page summarizes the Unity Lenses architecture as agreed by Neil and Mikkel.

== Glossary ==

To minimize the confusion we might want to explain what a lens actually is but instead let's all try and stick to the following glossary ([[attachment:unity-glossary.svg|.svg source]]):

{{attachment:unity-glossary.png}}

== Introduction ==

Neil has created a presentation/introduction for developers.

{{attachment:Unity_Places_Developer_Guide.pdf}}

Here are some [[Unity/Places/Ideas/|ideas]] we have for Unity places.

=== Plan for Natty ===

Lenses are something that we can put in extras.ubuntu.com since they're small and can be quick moving and we want people making lots of them. This gives us the rest of the development cycle and Natty's lifecycle for people to submit Places that people can run on their stable release without digging around for PPAs.

 * Quickly needs to be adapted for /opt-like things, after feature freeze: Didrocks

== Place Daemons ==

=== Lens Daemons from 1000 Feet ===
A lens daemon expose a list of place entries over DBus. Each entry holds enough data and metadata for Unity to render it. Each place entry has it's own icon in the places bar.

Each lens has a DBus API that exposes it's sections, groups, and core datamodel. It also defines which rendering mode Unity must use to render each group. Rendering modes includes can include fx. tiled, cover flow, and how many rows to show.

=== Registration ===
Places are registered in Unity by adding a special {{{.place}}}-file into {{{$prefix/share/unity/places}}}. The {{{.place}}} file is a keyfile that contains the following
{{{
[Place]
DBusName=Well known bus name the place can be found under
DBusObjectPath=DBus object path the place object lives at

[Entry:Stuff]
DBusObjectPath=DBus path for the place entry (must be a direct child of Places' DBusPath)
Icon=Icon for the place entry
Name=Display name for the entry
Description=Short description of what the entry provides. Suitable for display as tooltip or other
SearchHint=Message that will be displayed in the search box
ShowGlobal=true|false (defaults to true)
ShowEntry=true|false (defaults to true)
Shortcut=a


[Entry:OtherStuff]
...
}}}

The contents of the {{{.place}}}-file allows Unity to draw an icon in the places bar. The actual place daemon will be started lazily.

The {{{[Place]}}} group in the key file contains the basic info necessary for DBus-activating the place daemon.

Each {{{[Entry:*]}}} group contains the info necessary for Unity to render an icon in the places bar. Note that it can not be guaranteed this entry info is precise. When the daemon comes up some entries may be added or removed compared to this info.

/!\ Show`Global determines whether the Lens shows up in the user's Dash or not. Currently we don't have a method for allowing users to determine this, so we recommend that all Lens authors set this to false so as not to clutter a user's Dash with all the places they have installed. We'll sort this out in 11.10.

A place that requires {{{ShowEntry=False}}} is the Control Center place which is searchable from the home screen, but not otherwise displayed. The {{{Shortcut}}} key defines which keyboard key you must press together with {{{<super>}}} to open the place. In the example above the user must press {{{<super>-a}}} to activate the place. The {{{<super>}}} key to launch apps shortcut is defined in compiz itself.

=== Example Code ===
You can find a full working example of a place implementation in [[https://launchpad.net/unity-place-sample|launchpad.net/unity-place-sample]].

The full source code for the files- and applications places can be found on [[https://launchpad.net/unity-place-files|launchpad.net/unity-place-files]] and [[https://launchpad.net/unity-place-applications|launchpad.net/unity-place-applications]]. These are quite a lot more complex than the sample above, but exercises the full feature set described in this specification if you would like to see working examples.

== Renderers ==
Each group is rendered by some specific "renderer" inside the Unity shell. The renderer is defined with a !CamelCased namespaced type string, ala GLib GTypes, eg. {{{UnityDefaultRenderer}}}. If the renderer type is the empty string the group or entry should not be displayed.

Each place entry has a default renderer specified (in its {{{renderer_info.default_renderer}}} property). This may be overridden on a per-group basis by the groups model specified (in its {{{renderer_info.groups_model}}}).

Examples of renderers could include the default tiled rendering, coverflow, or a webkit-backed HTML renderer. At some point we will also make renderers a pluggable part of the Unity shell.

==== UnityDefaultRenderer ====
Renders rows in the results model using the {{{icon_hint}}} column with the {{{display_name}}} column underneath it. The icon should be a serialized GIcon (using {{{g_icon_to_string()}}}). Falls back to using the icon for the {{{mimetype}}} column if the {{{icon_hint}}} is not set. Renders one row of icons by default. FIXME: We need a way to set a hint for showing {{{N}}} rows or all rows.

==== UnityEmptySectionRenderer ====
Used to convey to the user that the current section is empty. The first row of the results model contains the message to the user in the {{{display_name}}} column. Any subsequent rows contain optional alternative actions the user may activate. These actions are URI encoded, so fx. a ''Try searching for 'pony' on Google''-alternative might be encoded with {{{http://www.google.com/#q=pony}}} in the {{{uri}}} column of the second row and the string {{{"Try searching for <i>pony</i> on Google"}}} in the {{{display_name}}} column.

==== UnityEmptySearchRenderer ====
Used exactly like {{{UnityEmptySectionRenderer}}} but informs the user that the current search did not match anything.

==== UnityShowcaseRenderer ====
Used for the {{{Most Used}}} category in the applications place. Has a slightly larger icon than the default renderer.

==== UnityFileInfoRenderer ====
Used in files place to show search results. Expects a valid URI, and will show extra information such as the parent folder and whatever data is in the comment column of the results.

== DBus API for Place Daemons ==

=== Data Structures ===

<<Anchor(RendererInfo)>>
==== RendererInfo ====
There are a few datastructures we pass over the wire. Firstly we have {{{RendererInfo}}} struct with signature {{{(sss{ss})}}}. We use {{{R}}} as shorthand for this struct:
{{{
  string default_renderer (name defining the default rendering mode for this place entry)
  string groups_model (DBus name of a Dee.Model)
  string results_model (DBus name of a Dee.Model)
  map<string,string> renderer_hints
}}}

 (!) If the entry wide renderer needs to be changed (fx. after the view calls {{{SetActiveSection()}}} to have a specialized "photo collection view" or something) the entry emits the {{{RendererInfoChanged}}} signal with the updated renderer info.

If {{{default_renderer}}} is the empty string then the contents of the entry should be ignored by the Unity shell. This way place entries can control whether they appear in the global search and/xor/nor in the places bar. This behaviour reflects the {{{ShowGlobal}}} and {{{ShowEntry}}} keys in the {{{Entry}}} group of the {{{.place}}} file.

<<Anchor(RendererInfoHints)>>
==== RendererInfo Hints ====
You can specify the following hints to a renderer:

 * {{{ExpandedGroups}}} - a space separated list of integer values specifying a set of group ids for groups which should always by fully expanded (ie all rows rendered). Fx {{{"0 3 4"}}} would mean that group ids 0, 3, and 4 should always have all their members visible.

<<Anchor(PlaceEntryInfo)>>
==== PlaceEntryInfo ====
And a {{{PlaceEntryInfo}}} struct with signature {{{(sssuasbs{ss}RR)}}}. We use {{{P}}} as a shorthand signature:
{{{
  string dbus_path, (DBus path to the Entry object)
  string name,
  string icon,
  uint position, (the position to render the entry at in the places bar)
  string[] mimetypes, (mimetypes accepted for drag-n-drop)
  bool sensitive,
  string sections_model (DBus name of a Dee.Model)
  map<string,string> hints,
  RendererInfo entry_renderer_info
  RendererInfo global_renderer_info
}}}
<<Anchor(PlaceEntryInfoHints)>>
==== PlaceEntryInfo Hints ====
 . The following hints are defined for the {{{PlaceEntryInfo}}}
  * {{{UnitySectionStyle}}} - if set to {{{"breadcrumb"}}} the {{{sections_model}}} should be displayed as a breadcrumb trail instead of as separate buttons
  * {{{UnityPlaceBrowserPath}}} - if set, will contain a DBus object path pointing to an object on the place daemon implementing the interface [[#com.canonical.Unity.PlaceBrowser|com.canonical.Unity.PlaceBrowser]]
  * {{{UnityActiveSection}}} - optionally indicates which section the place daemon is currently showing. The value is the section id encoded in a string, eg {{{"3"}}}.This is used when returning from browsing mode to a particular section. This hint is not set when the section changes because of a call to {{{SetActiveSection()}}}.
  * {{{UnityExtraAction}}} - contains a string serialized {{{GIcon}}} which should be rendered as a button. When clicked Unity should call {{{com.canonical.Unity.Activation.Active()}}} with the argument being the string {{{"."}}}. An example usage is a "Browse This Folder in Nautilus" button rendered at the right of the path bar when in folder browsing mode. ''FIXME: This is an oversimplified way of extending the model with capabilities, designed this way to keep wire compatibility. We should definitely revisit this for Natty''
= Overview =

To minimise confusion and to get you used to the terms used in the rest of this page, it's worth having a quick overview of the Dash, and the terms used to describe it:

{{attachment:unity-lenses-overview.png}}

= Introduction =

One of Unity's main features is the Dash. The Dash allows the user to quickly search for information both locally (installed applications, recent files, bookmarks, etc) and remotely (Twitter, Google Docs, etc).

The Dash achieves this by having one or more '''Lenses''' that each are responsible for providing one category of search results for the Dash. The user may search the Lens either through the Dash home screen (called ''global search'') or through the Lens' own page in the Dash by clicking on the Lens' icon on the ''Lens Bar''.

On it's own, a Lens is not very useful. It can guarantee an icon on the Lens Bar and a page in a the Dash but nothing will happen when the user searches it. This is because '''a Lens does not actually perform any searches itself'''. Instead, a Lens will have one or more '''''Scopes''''' which are the actual engines that do the searching for it.

This means that it is possible for new Scopes to supplement results of existing Lenses i.e. a Google Docs Scope can supplement the results of the default Zeitgeist Scope in the Files Lens, with results appearing side-by-side. Also, this means it is now possible for one Lens to have many Scopes i.e. the Music Lens can have a Banshee, UPNP and Spotify Scopes providing results to it.

= Architecture =

If you consider a Lens with two Scopes (e.g. Music Lens, a Banshee Scope and a Spotify Scope) this will generally mean that there will be four processes involved in searching the category of content that the Lens provides:

 1. The Dash itself (i.e Unity)
 2. The Lens daemon
 3. The first Scope daemon (Banshee Scope)
 4. The second Scope daemon (Spotify Scope)

It is then the job of the Lens to keep all these processes synchronised with each other. Luckily this happens behind the scenes and, as a Lens or Scope author, you will not have to get involved. However, it is nevertheless worth noting the complexity:

{{attachment:unity-lenses-architecture.png}}

= Creating a Lens =

Creating a Lens requires:

 1. A .lens file so Unity can find and load the Lens
 2. A daemon that uses a well-known name on D-Bus
 3. A D-Bus .service file that let's Unity auto-activate the Lens when it's ready to (this is optional if your Lens is long-running anyway, but is probably required for 99% of the Lenses that will be written).

== Responsibilities of a Lens ==

A Lens is responsible for providing the following information to Unity:

 * Which Categories it supports
 * Which Filters it supports
 * What to display in the search entry as the search hint
 * Whether the Lens should show in the Lens Bar
 * Whether then Lens should participate in searches from the Dash Home view (global search).

== Your Lens ID ==

Due to do the way Unity and libunity export Lenses and find Scopes, it is essential to choose a ID for your Lens. This ID is used in various places and should be specific enough to avoid conflicts with other lenses. The id can be made up of alphanumeric characters and, additionally, dashes.

Some examples:

|| Lens || Lens ID ||
|| Files || files ||
|| Applications || applications ||
|| Google Docs || gdocs ||

Through this document, we will refer to the Lens ID either as ''Lens ID'' or ''$lens_id'', please substitute the real Lens ID for those.

== A .lens file ==

The .lens file is critical for Unity as it allows Unity to load some information about the Lens before loading it (great for optimising startup) and, essentially, it tells Unity how to activate/find your daemon on D-Bus.

Here is an example of a .lens file:
{{{#!highlight ini
[Lens]
DBusName=net.launchpad.Lens.MyLens
DBusPath=/net/launchpad/lens/mylens
Name=My Lens
Icon=/path/to/mylens.svg
Description=A Lens to search my stuff
SearchHint=Search your stuff
Shortcut=m

[Desktop Entry]
X-Ubuntu-Gettext-Domain=my-lens
}}}

 * '''DBusName:''' A well known D-Bus name for finding your Lens.
 * '''DBusPath:''' The D-Bus path that the Lens is exported on.
 * '''Name:''' The name of your Lens.
 * '''Icon:''' The icon that should show on the Lens Bar and/or the Launcher. SVG is preferable.
 * '''Description:''' A single-line description of your Lens. This isn't used visually but could be used for accessibility.
 * '''SearchHint:''' The search hint that shows in the Dash search entry when your Lens is focused.
 * '''Shortcut:''' The preferred shortcut to be used to call up your Lens (i.e. =m would translate to <Super>+m). '''''This is not guarenteed if the user has other shortcuts already set for your key combination'''''
 * '''X-Ubuntu-Gettext-Domain:''' This tells Unity which is the right translation domain for your Lens to find the translatable strings in this Lens file.

The .lens file should be installed into the same prefix as Unity itself in this format: `/prefix/where/unity/was/installed/share/unity/lenses/$lens_id/$lens_id.lens`

So, if Unity was installed in /usr and your Lens ID was ''mylens'', then the .lens file would be installed in
`/usr/share/unity/lenses/mylens/mylens.lens`

== A D-Bus service file ==

{{{#!highlight ini
[D-BUS Service]
Name=net.launchpad.Lens.MyLens
Exec=/my/daemon/install/prefix/lib/mylens/my-lens-daemon
}}}

 * '''Name:''' A well known D-Bus name for finding your Lens (should be same as DBusName above).
 * '''Exec:''' The executable for your daemon that will load and register this name on D-Bus.

D-Bus .service files should be installed into `/usr/share/dbus-1/services`.

== Using the Lens Object ==

Once you have registered your daemon on D-Bus and named the connection, the next thing to do is create and initialise your Lens object. This is done through the Lens object in [[libunity | https://launchpad.net/libunity]].

=== Creating the Lens and setting default properties ===

{{{#!highlight csharp
{
  // When creating a Lens object, you must tell it the DBusPath you
  // used in the .lens file, and the Lens ID you have chosen.
  this.lens = new Unity.Lens("/net/launchpad/lens/mylens", "mylens");

  // Translatable search hint.
  lens.search_hint = _("Search your stuff");

  // Does it appear in the Lens Bar?
  lens.visible = true;

  // Should Unity include the Lens in global search results?
  lens.search_in_global = true;

  // Add the default categories (see below)
  populate_categories();

  // Add the default filters (see below)
  populate_filters();

  // Now that all the default properties are set, export the Lens
  // so Unity can find it.
  lens.export();
}
}}}

=== Adding Categories ===

You should add at least one Category to your Lens. Categories allow you to create distinct partitions between different types of data in your Lens (i.e. Music Lens has Songs, Albums and Available to Purchase). When Scopes add results, they indicate which Category the result belongs in by sending an integer referencing the order the Category was added to the Lens. '''''Note''''': This will be made easier for Scopes to reference with a tool that is under development.

For example, a Scope for the Music Lens would add a song with the third column (the category column) to 0, a album would have 1 and a purchasable song would have 2.

When adding a Category, you can choose it's icon, it's name and, finally, how you would like Unity to render the results within that Category. For 11.10, Unity supports two renderers: Vertical Tile and Horizontal Tile:

{{attachment:unity-lenses-categories.png}}

{{{#!highlight csharp
private void populate_categories ()
{
  GLib.List<Unity.Category> categories = new GLib.List<Unity.Category> ();
  File icon_dir = File.new_for_path (ICON_PATH);

  var cat = new Unity.Category (_("Songs"),
                                new FileIcon (icon_dir.get_child ("group-mostused.svg")),
                                VERTICAL_TILE);
  categories.append (cat);

  cat = new Unity.Category (_("Albums"),
                            new FileIcon (icon_dir.get_child ("group-installed.svg")),
                            HORIZONTAL_TILE);
  categories.append (cat);

  cat = new Unity.Category (_("Available to Purchase"),
                            new FileIcon (icon_dir.get_child ("group-available.svg")),
                            VERTICAL_TILE);
  categories.append (cat);

  lens.categories = categories;
}

}}}


=== Adding Filters ===

While a Lens is not required to have any Filters, it is thought that most Lenses will benefit from an easy way for the user to be able to refine their search criteria.

It is up to the Lens to create the Filters for Unity to renderer, and the Scopes to query their state while searching to effect the results.

In 11.10, there are four different types of Filters:

==== CheckOption ====

A CheckOption Filter allows the user to select one or more options from the provided list. In practice, this means that they could search for Folders and PDFs in the example below.

{{attachment:unity-lenses-checkoption.png}}

{{{#!highlight csharp
{
  // The first parameter should be a unique name for this filter
  // it is used by Scopes to lookup the filter and it's state
  // so don't have two filters with the same name!
  // The second argument is what the user will see in Unity
  var filter = new CheckOptionFilter("type", _("Type"));

  // CheckOption, RadioOption, MultiRange are all OptionFilter
  // subclasses. The OptionFilter subclass abstracts the concept
  // of "adding options" to a Filter. For example, every check
  // button or "option" in the CheckOptionFilter is added in the
  // same way as it is for the RadioOptionFilter or the Multi-
  // OptionFilter.
  //
  // The add_option takes an unique id for this option as its
  // first argument (which allows a Scope to check it's state
  // easily), and the user-visible name as it's second.
  // There is a third, optional, argument of a GIcon, but it
  // is unused in 11.10.
  filter.add_option ("document", _("Document"));
  filter.add_option ("folder", _("Folder"));
  filter.add_option ("pdf", _("PDF"));

  // etc
}
}}}

==== RadioOption ====

The RadioOption Filter allows the user to select only one out of the given options, like a radio group behaves in a widget toolkit. So, with the example below, the user could choose Today or Last Week, but not both.

{{attachment:unity-lenses-radiooption.png}}

{{{#!highlight csharp
{
  var filter = new RadioOptionsFilter ("last-modified", _("Type"));
  filter.add_option ("today", _("Today"));
  filter.add_option ("yesterday", _("yesterday"));
  filter.add_option ("last-week", _("Last Week"));
}
}}}

==== MultiRange ====

The MultiRange Filter allows the user to choose a range from within the provided options. So, in the example below, if the user clicked on 1MB and 100 MB, 1MB, 10MB and 100MB would be active. If the user then clicked on 10MB, one 1MB and 10MB would be active.

{{attachment:unity-lenses-multirange.png}}

{{{#!highlight csharp
{
  var filter = new MultiRangeFilter ("size", _("Size"));
  filter.add_option ("<1MB", _("<1MB"));
  filter.add_option ("1MB", _("1MB"));
  filter.add_option ("10MB", _("10MB"));
  filter.add_option ("100MB", _("100MB"));
  filter.add_option ("1GB", _("1GB"));
}
}}}

==== Ratings ====

The Ratings Filter is the simplest of the bunch, allowing the user to choose an up-to-five star rating to refine the search. This works well for content where rating information is available, like Applications in the software centre, or songs in Banshee.

{{attachment:unity-lenses-ratings.png}}

{{{#!highlight csharp
{
  var filter = new RatingsFilter("rating", _("Rating"));
}
}}}

==== Complete Example ====

{{{#!highlight csharp
{
  GLib.List<Unity.Filter> filters = new GLib.List<Unity.Filter> ();

  var filter = new CheckOptionFilter("type", _("Type"));
  filter.add_option ("document", _("Document"));
  filter.add_option ("folder", _("Folder"));
  filter.add_option ("pdf", _("PDF"));
  filters.append (filter);

  filter = new RadioOptionsFilter ("last-modified", _("Type"));
  filter.add_option ("today", _("Today"));
  filter.add_option ("yesterday", _("yesterday"));
  filter.add_option ("last-week", _("Last Week"));
  filters.append (filter);

  filter = new MultiRangeFilter ("size", _("Size"));
  filter.add_option ("<1MB", _("<1MB"));
  filter.add_option ("1MB", _("1MB"));
  filter.add_option ("10MB", _("10MB"));
  filter.add_option ("100MB", _("100MB"));
  filter.add_option ("1GB", _("1GB"));
  filters.append (filter);

  filter = new RatingsFilter("rating", _("Rating"));
  filters.append (filter);

  lens.filters = filters;
}
}}}

== What else? ==

Nothing much, really. A Lens, once it's been exported, just needs to hang-around for some Scopes to join in the fun and for Unity to start searching it. The synchronization between the processes is handled internally by libunity.

In the future we want to allow the Lens to have a greater say in the results it presents (i.e. being able to de-dupe results across different Scopes, etc), however for 11.10, as a Lens author, your job is done at this point!

= Creating a Scope =

== Responsibilities of a Scope ==

The Scope is responsible for the following:

 * Providing search results to it's parent Lens taking into account the state of the Filters at the time of the search.
 * Providing global search results that ignore the state of the filters.
 * Indicating to it's parent Lens that it has finished it's normal or global search.
 * Taking responsibility for activating any result it owns when the user clicks on it.

== Registration ==

There are two types of Scope that only differ in how they are registered with their parent Lens (the Lens they provide search data for).

A Local Scope is a scope that is created inside the same process as the Lens. Normally this would be done to ensure the Lens is useful without installing additional software or, if the Lens was very specialised, then to just simplify writing it by only needing to create and install on daemon. An example would be someone writing a Google Docs Lens that also included the Scope that interfaces with the Google Docs API.

A Remote Scope is a scope that plugs into an existing Lens from a separate process. This type of Scope would run in it's own daemon. An example is if someone were to write a Last.fm Scope for the Music Lens.

=== Local Scope ===

As the Scope has been created and is running in the same process as the parent Lens, it does not need to go through the same registration as a Remote Scope, instead you simply need to do:

{{{#!highlight csharp
{
  // lens was setup before this point
  lens.add_local_scope (the_local_scope);

  lens.export();
}
}}}

This tells the Lens that you want to use a Scope that is running the same process as itself, and the Lens will do what is necessary to let that happen.

=== Remote Scope ===

For a Remote Scope to be found by it's Lens, it needs to install a .scope file to give the Lens enough details to launch/connect to it.

An example .scope file:

{{{#!highlight ini
[Scope]
DBusName=net.launchpad.Scope.lastfm
DBusPath=/net/launchpad/scope/lastfm
}}}

This is simply telling which well-know D-Bus name it can find the Scope on, and the path to the Scope object at that address.

The .scope file should be installed in the folder of the Scope's parent Lens. So, if this Scope was for the Music Lens, it would be installed into `/usr/share/unity/lenses/music/lastfm.scope`.

It is worth mentioning that the name of the scope file is not as important as a .lens file, but it is useful to use a specific enough name to avoid conflicts with other scopes.

'''''Note:''''' A Remote Scope should install a D-Bus .service file much like a Lens does. This allows auto-activation if it is not already running when the Lens starts.

== Using the Scope Object ==

The Scope object is your conduit to the current state of Unity. The Scopes parent Lens will keep the Scope updated with the current state, and it is the owner of the Scope's job to keep both the results models (normal and global) updates to reflect this state.


=== Creating & Initialising the Scope ===

{{{#!highlight csharp
{
  // When creating a Scope object, only the dbus path that you
  // would like it exported at is required. This should match
  // the DBusPath property in the .scope file if this a remote
  // scope. Otherwise any, valid, D-Bus path will do.
  this.scope = new Unity.Scope( ("/net/launchpad/scope/lastfm");

  // You can choose at a per-scope level if you want to be
  // involved in global searches. This means you can opt-out
  // of global searches even if your parent Lens has opted-in.
  scope.search_in_global = true;

  // The key thing that you are doing with the Scope object is
  // listening for, and reacting to, any state changes.
  // Therefore it is essential to connect up to all the important
  // signals and property-change events.
  // NOTE: The handlers are explained in the specific sections below.

  // First up, we want to be notified whenever the user changes the
  // search string in our Lens in the Dash
  scope.notify["active-search"].connect(on_search_changed);

  // Next, we want to react to changes when the user searches from
  // the home screen in the Dash
  scope.notify["active-global-search"].connect(on_global_search_changed);

  // If the user twiddles any of the filters, we want to update our
  // search to reflect the change
  scope.filters_changed.connect(on_filters_changed);

  // Finally, we want the user to be able to actually click on a
  // result and have something happen, so we connect to the
  // activate-uri signal, allowing us to react to the click and
  // inform Unity of the result.
  scope.activate_uri.connect(on_uri_activated);

  // If this is a remote Scope, we'll export it, otherwise for a Local
  // Scope, this is where we would use the lens.add_local_scope to let
  // the Lens know it exists.
  scope.export();
}
}}}

=== Reacting to the active-search changing ===

When the active-search changes, you can query the current `Unity.LensSearch` of the Scope via the active_search property. From that object, you can query the current search term and use it to refresh your search results:

{{{#!highlight csharp
private void on_search_changed(Object obj, ParamSpec pspec)
{
  Unity.LensSearch? search = scope.active_search;

  if (!search)
    return;

  debug("The current search term is: %s", search.search_string);
  update_search(search);
}
}}}

=== Reacting to the active-global-search changing ===

This works much the same as reacting to active-search, but with change of property names:

{{{#!highlight csharp
private void on_search_changed(Object obj, ParamSpec pspec)
{
  Unity.LensSearch? search = scope.active_global_search;

  if (!search)
    return;

  debug("The current global search term is: %s", search.search_string);
  update_global_search(search);
}
}}}

=== The magic "" search term (the Default View) ===

In the world of the Dash, receiving a search term that equals "" means that you should populate your results model with default results. These are the results that the user sees when they switch to a Lens in the Dash but do not search for anything.

For example, the Applications Lens, when given a search term of "", will populate it's three Categories with the most recently used applications, all installed applications, and a selection of available, highly rated, applications.

You should always strive to have something useful in the default view.

=== Reacting to the filters-changed signal ===

Depending on how the content you are searching takes into account Filters, which could be that you need to update a setting on a library in direct response to the filters changing or, in the opposite case, you just need to take them into account when you perform the search (i.e. add them to the end of a URL), how you react to the filters changing will differ.

For the latter, and more general, case, you would chain the filters-changed signal to the update_search callback, and let it just perform another search (as it will take into account the current filter state).

In the second case you might have an extra step in between to set the options on your datastore before updating the search.

'''''NOTE:''''' Although you might feel it's better to react to the filters-changed signal by analysing your current result set and just removing results that don't fit, unless the checking is very simple, it might just be better to do a new query.

=== Querying Filter State ===

A Scope is expected to respect the Filters of it's parent Lens. This means that the Scope should know the ids of the Filters and their options so it can understand the state ('''''Note:''''' There is no easy way to do this outside of reading the Lens' source currently, a tool is in the works to simplify the process).

Although you can iterate through the list of Filters the Scope has, it's often easier/makes more sense to query the filter directly and then query it's state:


''All examples reference those used in the Lens "Adding Filters" section above ''

==== Querying CheckOption State ====

{{{#!highlight csharp
{
  // Get the type CheckOption Filter from the Scope
  var filter = scope.get_filter("type") as CheckOptionFilter;
Line 148: Line 483:
=== com.canonical.Unity.Place ===
The place daemon exposes the interface {{{com.canonical.Unity.Place}}} on the advertised {{{DBusName}}} and {{{DBusObjectPath}}} found in its {{{.place}}} file:
{{{
  method GetEntries (out aP)
  signal EntryAdded (out P)
  signal EntryRemoved (out s) (the DBus path to the Entry object)
}}}

=== com.canonical.Unity.PlaceEntry ===
Each listed entry exposes the interface {{{com.canonical.Unity.PlaceEntry}}}

{{{
  /* Global search from the home screen. Populates the models
   * defined in EntryInfo.entry_renderer_info */
  method SetGlobalSearch (in s search_string,
                          in {ss} hints,
                          out u hit_count)

  /* Set whether or not this section is currently being displayed */
  method SetActive (in b active);

  /* Search withing the currently active section */
  method SetSearch (in s search_string,
                    in {ss} hints);

  /* Request that a given section be represented in the model
   * defined by EntryInfo.entry_renderer_info */
  method SetActiveSection (in u active_section_id); (defaults to 0)

  signal EntryRendererInfoChanged (R renderer_info);

  signal GlobalRendererInfoChanged (R renderer_info);

  /* Emitted fx. when the hints change or the name of the sections model change.
   * Both may occur fx. when browsing into a folder.
   * The included struct is the fields of the PlaceEntryInfo struct, but
   * without the two RenderingInfo structs */
  signal PlaceEntryInfoChanged (in (sssuasbs{ss}) place_entry_info);

  /* Optionally emitted by a place entry once it has completed all operations for
   * a SetSearch, SetGlobalSearch, or SetActiveSection request and fully updated
   * the result model(s) accordingly. Places are not required to emit this signal
   * and the Unity shell must assume that they do */
  signal SearchFinished (in u section, in s search, in a{ss} hints)
}}}

=== Results Model ===
The value defined in {{{PlaceEntryInfo.results_model}}} is the bus name of a {{{Dee.Model}}} that holds the live results to be displayed in the main view. ''The columns of this model are specific for the renderer_name defined in the [[#RendererInfo|RendererInfo]] struct''. The column types for the {{{"UnityDefaultRenderer"}}} is:
{{{
  s uri
  s icon_hint
  u group_id
  s mimetype
  s display_name
  s comment
}}}

=== Groups Model ===
The bus name of the groups model is defined in {{{PlaceEntryInfo.groups_model}}}. This is another {{{Dee.Model}}}. It contains the description on how to render the individual group found in the results model.

The ''group_id'' in the results model maps to the row index in the groups model. The columns of the groups model are:
{{{
  s group_renderer (a Unity-specific rendering mode for this group)
  s display_name
  s icon_hint
}}}

=== Sections Model ===
Yet another {{{Dee.Model}}} which is defined by the name in {{{PlaceEntryInfo.sections_model}}}. This model defines the section headers for the entry displayed just below the places bar.

The row index in the model is used as the ''section_id'' - see the {{{PlaceEntry.SetActiveSections}}} method. The default active sections is always section 0.

The model columns are:
{{{
  s display_name
  s icon
}}}

= Activation Hooks =
Places can register "activation hooks" for certain types of URIs and/or mimetypes. This is done by defining a {{{[Activation]}}} group in the {{{.place}}} file like:

{{{
// normal .place stuff

[Activation]
URIPattern=Regex that URIs must match in order to trigger this activation
MimetypePattern=Regex that mimetypes must match in order to trigger this activation
Priority=0 // G_PRIORITY_{LOW,HIGHT,DEFAULT}
}}}

The activation service exposes a {{{com.canonical.Unity.Activation}}} interface on the same bus name and object path as the place service specified in the {{{[Place]}}} group.

=== com.canonical.Unity.Activation ===
{{{
  /**
   * Try to activate a URI
   * @uri: The URI to activate
   *
   * Returns: 0 if the URI was not activated, 1 if it was activated and dash
   * should not hide, and 2 if the URI was activated and the dash
   * should hide
   */
  Activate (in s uri, out u handled)
}}}

= Browsing. Aka Back/Forward =
Browsing buttons can be presented by the Unity rendering shell if the {{{UnityPlaceBrowserPath}}} hint is set on the [[#PlaceEntryInfo|PlaceEntryInfo]] struct. The {{{UnityPlaceBrowserPath}}} hint contains the DBus object path for an object in the place daemon implementing the {{{com.canonical.Unity.PlaceBrowser}}} interfafce.

This hint will be set by the model when you activate a folder (which causes the model to browse into that folder) - in which case also the {{{UnitySectionStyle}}} hint will be set to {{{"breadcrumb"}}}.

When the place daemon leaves browsing mode, it will often return to some specific section. In order to tell the Unity rendering shell which section to highlight the place daemon sets the hint {{{UnityActiveSection}}} to the id of the active section.

=== com.canonical.Unity.PlaceBrowser ===
The browsing is centered around a "states" object represented by a {{{a(bs)}}}. The array consists of two states. The 0th state being the "!GoBack state" and the 1st being the "!GoForward state". The {{{(bs)}}} state struct is interpreted as:

 . '''b''' : Whether or not this state is enabled or not. If False this state should be rendered as "disabled". Fx. when there's nothing in your backwards history.
 . '''s''' : A string optionally used as a tooltip or comment on what happens if you activate that state. Eg. "Go back to http://youtube.com"

{{{
  GoBack (out a(bs) state)
  GoForward (out a(bs) state)
  GetState (out a(bs) state)
}}}
  // As we can have none, one or many options enabled, we need to iterate
  // through and see which ones are
  foreach (Unity.FilterOption option in filter.options)
  {
    if (option.active)
    {
      // We are pretending that our websource uses exactly the same naming as
      // the ids our Lens uses. Ahh, to write pretend software...
      if (url == "")
        url="&type=" + option.id;
      else
        url+="+" + option.id;
    }
  }
}
}}}

==== Querying RadioOption State ====

{{{#!highlight csharp
{
  // Get the last-modified RadioOption Filter from the Scope
  var filter = scope.get_filter("last-modified") as RadioOptionFilter;

  // Radio option is way simpler, we just need to query the active option
  // (if one exists) and do something for that
  if (filter.get_active_option())
    url += "&modified=" + filter.get_active_option().id;
}
}}}

==== Querying MultiRange State ====

{{{#!highlight csharp
{
  // Get the size MultiRange Filter from the Scope
  var filter = scope.get_filter("size") as MultiRangeFilter;

  // As MultiRange works by having an option at the first and last positions,
  // we just need to make sure it has a valid FilterOption for both the first
  // and last positions and use that info to sent the range we want.
  if (filter.get_first_active() && filter.get_last_active())
  {
    url += "&min-size=" + filter.get_first_active().id + "&max-size=" + filter.get_last_active().id;
  }
}
}}}

==== Querying Ratings State ====

{{{#!highlight csharp
{
  // Get the ratings Ratings Filter from the Scope
  var filter = scope.get_filter("ratings") as RatingsFilter;
  url += "&min-rating=" + "%f".printf(filter.rating);
}
}}}

=== Handling Activation ===

=== Full example ===

This is a testing page for the new Lenses API docs, and might already be outdated when you read it!

Unity > Lenses

Overview

To minimise confusion and to get you used to the terms used in the rest of this page, it's worth having a quick overview of the Dash, and the terms used to describe it:

Introduction

One of Unity's main features is the Dash. The Dash allows the user to quickly search for information both locally (installed applications, recent files, bookmarks, etc) and remotely (Twitter, Google Docs, etc).

The Dash achieves this by having one or more Lenses that each are responsible for providing one category of search results for the Dash. The user may search the Lens either through the Dash home screen (called global search) or through the Lens' own page in the Dash by clicking on the Lens' icon on the Lens Bar.

On it's own, a Lens is not very useful. It can guarantee an icon on the Lens Bar and a page in a the Dash but nothing will happen when the user searches it. This is because a Lens does not actually perform any searches itself. Instead, a Lens will have one or more Scopes which are the actual engines that do the searching for it.

This means that it is possible for new Scopes to supplement results of existing Lenses i.e. a Google Docs Scope can supplement the results of the default Zeitgeist Scope in the Files Lens, with results appearing side-by-side. Also, this means it is now possible for one Lens to have many Scopes i.e. the Music Lens can have a Banshee, UPNP and Spotify Scopes providing results to it.

Architecture

If you consider a Lens with two Scopes (e.g. Music Lens, a Banshee Scope and a Spotify Scope) this will generally mean that there will be four processes involved in searching the category of content that the Lens provides:

  1. The Dash itself (i.e Unity)
  2. The Lens daemon
  3. The first Scope daemon (Banshee Scope)
  4. The second Scope daemon (Spotify Scope)

It is then the job of the Lens to keep all these processes synchronised with each other. Luckily this happens behind the scenes and, as a Lens or Scope author, you will not have to get involved. However, it is nevertheless worth noting the complexity:

Creating a Lens

Creating a Lens requires:

  1. A .lens file so Unity can find and load the Lens
  2. A daemon that uses a well-known name on D-Bus
  3. A D-Bus .service file that let's Unity auto-activate the Lens when it's ready to (this is optional if your Lens is long-running anyway, but is probably required for 99% of the Lenses that will be written).

Responsibilities of a Lens

A Lens is responsible for providing the following information to Unity:

  • Which Categories it supports
  • Which Filters it supports
  • What to display in the search entry as the search hint
  • Whether the Lens should show in the Lens Bar
  • Whether then Lens should participate in searches from the Dash Home view (global search).

Your Lens ID

Due to do the way Unity and libunity export Lenses and find Scopes, it is essential to choose a ID for your Lens. This ID is used in various places and should be specific enough to avoid conflicts with other lenses. The id can be made up of alphanumeric characters and, additionally, dashes.

Some examples:

Lens

Lens ID

Files

files

Applications

applications

Google Docs

gdocs

Through this document, we will refer to the Lens ID either as Lens ID or $lens_id, please substitute the real Lens ID for those.

A .lens file

The .lens file is critical for Unity as it allows Unity to load some information about the Lens before loading it (great for optimising startup) and, essentially, it tells Unity how to activate/find your daemon on D-Bus.

Here is an example of a .lens file:

   1 [Lens]
   2 DBusName=net.launchpad.Lens.MyLens
   3 DBusPath=/net/launchpad/lens/mylens
   4 Name=My Lens
   5 Icon=/path/to/mylens.svg
   6 Description=A Lens to search my stuff
   7 SearchHint=Search your stuff
   8 Shortcut=m
   9 
  10 [Desktop Entry]
  11 X-Ubuntu-Gettext-Domain=my-lens
  • DBusName: A well known D-Bus name for finding your Lens.

  • DBusPath: The D-Bus path that the Lens is exported on.

  • Name: The name of your Lens.

  • Icon: The icon that should show on the Lens Bar and/or the Launcher. SVG is preferable.

  • Description: A single-line description of your Lens. This isn't used visually but could be used for accessibility.

  • SearchHint: The search hint that shows in the Dash search entry when your Lens is focused.

  • Shortcut: The preferred shortcut to be used to call up your Lens (i.e. =m would translate to <Super>+m). This is not guarenteed if the user has other shortcuts already set for your key combination

  • X-Ubuntu-Gettext-Domain: This tells Unity which is the right translation domain for your Lens to find the translatable strings in this Lens file.

The .lens file should be installed into the same prefix as Unity itself in this format: /prefix/where/unity/was/installed/share/unity/lenses/$lens_id/$lens_id.lens

So, if Unity was installed in /usr and your Lens ID was mylens, then the .lens file would be installed in /usr/share/unity/lenses/mylens/mylens.lens

A D-Bus service file

   1 [D-BUS Service]
   2 Name=net.launchpad.Lens.MyLens
   3 Exec=/my/daemon/install/prefix/lib/mylens/my-lens-daemon
  • Name: A well known D-Bus name for finding your Lens (should be same as DBusName above).

  • Exec: The executable for your daemon that will load and register this name on D-Bus.

D-Bus .service files should be installed into /usr/share/dbus-1/services.

Using the Lens Object

Once you have registered your daemon on D-Bus and named the connection, the next thing to do is create and initialise your Lens object. This is done through the Lens object in https://launchpad.net/libunity.

Creating the Lens and setting default properties

   1 {
   2   // When creating a Lens object, you must tell it the DBusPath you
   3   // used in the .lens file, and the Lens ID you have chosen.
   4   this.lens = new Unity.Lens("/net/launchpad/lens/mylens", "mylens");
   5 
   6   // Translatable search hint.
   7   lens.search_hint = _("Search your stuff");
   8 
   9   // Does it appear in the Lens Bar?
  10   lens.visible = true;
  11 
  12   // Should Unity include the Lens in global search results?
  13   lens.search_in_global = true;
  14 
  15   // Add the default categories (see below)
  16   populate_categories();
  17 
  18   // Add the default filters (see below)
  19   populate_filters();
  20 
  21   // Now that all the default properties are set, export the Lens
  22   // so Unity can find it.
  23   lens.export();
  24 }

Adding Categories

You should add at least one Category to your Lens. Categories allow you to create distinct partitions between different types of data in your Lens (i.e. Music Lens has Songs, Albums and Available to Purchase). When Scopes add results, they indicate which Category the result belongs in by sending an integer referencing the order the Category was added to the Lens. Note: This will be made easier for Scopes to reference with a tool that is under development.

For example, a Scope for the Music Lens would add a song with the third column (the category column) to 0, a album would have 1 and a purchasable song would have 2.

When adding a Category, you can choose it's icon, it's name and, finally, how you would like Unity to render the results within that Category. For 11.10, Unity supports two renderers: Vertical Tile and Horizontal Tile:

   1 private void populate_categories ()
   2 {
   3   GLib.List<Unity.Category> categories = new GLib.List<Unity.Category> ();
   4   File icon_dir = File.new_for_path (ICON_PATH);
   5 
   6   var cat = new Unity.Category (_("Songs"),
   7                                 new FileIcon (icon_dir.get_child ("group-mostused.svg")),
   8                                 VERTICAL_TILE);
   9   categories.append (cat);
  10 
  11   cat = new Unity.Category (_("Albums"),
  12                             new FileIcon (icon_dir.get_child ("group-installed.svg")),
  13                             HORIZONTAL_TILE);
  14   categories.append (cat);
  15 
  16   cat = new Unity.Category (_("Available to Purchase"),
  17                             new FileIcon (icon_dir.get_child ("group-available.svg")),
  18                             VERTICAL_TILE);
  19   categories.append (cat);
  20 
  21   lens.categories = categories;
  22 }

Adding Filters

While a Lens is not required to have any Filters, it is thought that most Lenses will benefit from an easy way for the user to be able to refine their search criteria.

It is up to the Lens to create the Filters for Unity to renderer, and the Scopes to query their state while searching to effect the results.

In 11.10, there are four different types of Filters:

CheckOption

A CheckOption Filter allows the user to select one or more options from the provided list. In practice, this means that they could search for Folders and PDFs in the example below.

   1 {
   2   // The first parameter should be a unique name for this filter
   3   // it is used by Scopes to lookup the filter and it's state
   4   // so don't have two filters with the same name!
   5   // The second argument is what the user will see in Unity
   6   var filter = new CheckOptionFilter("type", _("Type"));
   7 
   8   // CheckOption, RadioOption, MultiRange are all OptionFilter
   9   // subclasses. The OptionFilter subclass abstracts the concept
  10   // of "adding options" to a Filter. For example, every check
  11   // button or "option" in the CheckOptionFilter is added in the
  12   // same way as it is for the RadioOptionFilter or the Multi-
  13   // OptionFilter.
  14   //
  15   // The add_option takes an unique id for this option as its
  16   // first argument (which allows a Scope to check it's state
  17   // easily), and the user-visible name as it's second.
  18   // There is a third, optional, argument of a GIcon, but it
  19   // is unused in 11.10.
  20   filter.add_option ("document", _("Document"));
  21   filter.add_option ("folder", _("Folder"));
  22   filter.add_option ("pdf", _("PDF"));
  23 
  24   // etc
  25 }

RadioOption

The RadioOption Filter allows the user to select only one out of the given options, like a radio group behaves in a widget toolkit. So, with the example below, the user could choose Today or Last Week, but not both.

   1 {
   2   var filter = new RadioOptionsFilter ("last-modified", _("Type"));
   3   filter.add_option ("today", _("Today"));
   4   filter.add_option ("yesterday", _("yesterday"));
   5   filter.add_option ("last-week", _("Last Week"));
   6 }

MultiRange

The MultiRange Filter allows the user to choose a range from within the provided options. So, in the example below, if the user clicked on 1MB and 100 MB, 1MB, 10MB and 100MB would be active. If the user then clicked on 10MB, one 1MB and 10MB would be active.

   1 {
   2   var filter = new MultiRangeFilter ("size", _("Size"));
   3   filter.add_option ("<1MB", _("<1MB"));
   4   filter.add_option ("1MB", _("1MB"));
   5   filter.add_option ("10MB", _("10MB"));
   6   filter.add_option ("100MB", _("100MB"));
   7   filter.add_option ("1GB", _("1GB"));
   8 }

Ratings

The Ratings Filter is the simplest of the bunch, allowing the user to choose an up-to-five star rating to refine the search. This works well for content where rating information is available, like Applications in the software centre, or songs in Banshee.

   1 {
   2   var filter = new RatingsFilter("rating", _("Rating"));
   3 }

Complete Example

   1 {
   2   GLib.List<Unity.Filter> filters = new GLib.List<Unity.Filter> ();
   3 
   4   var filter = new CheckOptionFilter("type", _("Type"));
   5   filter.add_option ("document", _("Document"));
   6   filter.add_option ("folder", _("Folder"));
   7   filter.add_option ("pdf", _("PDF"));
   8   filters.append (filter);
   9 
  10   filter = new RadioOptionsFilter ("last-modified", _("Type"));
  11   filter.add_option ("today", _("Today"));
  12   filter.add_option ("yesterday", _("yesterday"));
  13   filter.add_option ("last-week", _("Last Week"));
  14   filters.append (filter);
  15 
  16   filter = new MultiRangeFilter ("size", _("Size"));
  17   filter.add_option ("<1MB", _("<1MB"));
  18   filter.add_option ("1MB", _("1MB"));
  19   filter.add_option ("10MB", _("10MB"));
  20   filter.add_option ("100MB", _("100MB"));
  21   filter.add_option ("1GB", _("1GB"));
  22   filters.append (filter);
  23 
  24   filter = new RatingsFilter("rating", _("Rating"));
  25   filters.append (filter);
  26 
  27   lens.filters = filters;
  28 }

What else?

Nothing much, really. A Lens, once it's been exported, just needs to hang-around for some Scopes to join in the fun and for Unity to start searching it. The synchronization between the processes is handled internally by libunity.

In the future we want to allow the Lens to have a greater say in the results it presents (i.e. being able to de-dupe results across different Scopes, etc), however for 11.10, as a Lens author, your job is done at this point!

Creating a Scope

Responsibilities of a Scope

The Scope is responsible for the following:

  • Providing search results to it's parent Lens taking into account the state of the Filters at the time of the search.
  • Providing global search results that ignore the state of the filters.
  • Indicating to it's parent Lens that it has finished it's normal or global search.
  • Taking responsibility for activating any result it owns when the user clicks on it.

Registration

There are two types of Scope that only differ in how they are registered with their parent Lens (the Lens they provide search data for).

A Local Scope is a scope that is created inside the same process as the Lens. Normally this would be done to ensure the Lens is useful without installing additional software or, if the Lens was very specialised, then to just simplify writing it by only needing to create and install on daemon. An example would be someone writing a Google Docs Lens that also included the Scope that interfaces with the Google Docs API.

A Remote Scope is a scope that plugs into an existing Lens from a separate process. This type of Scope would run in it's own daemon. An example is if someone were to write a Last.fm Scope for the Music Lens.

Local Scope

As the Scope has been created and is running in the same process as the parent Lens, it does not need to go through the same registration as a Remote Scope, instead you simply need to do:

   1 {
   2   // lens was setup before this point
   3   lens.add_local_scope (the_local_scope);
   4 
   5   lens.export();
   6 }

This tells the Lens that you want to use a Scope that is running the same process as itself, and the Lens will do what is necessary to let that happen.

Remote Scope

For a Remote Scope to be found by it's Lens, it needs to install a .scope file to give the Lens enough details to launch/connect to it.

An example .scope file:

   1 [Scope]
   2 DBusName=net.launchpad.Scope.lastfm
   3 DBusPath=/net/launchpad/scope/lastfm

This is simply telling which well-know D-Bus name it can find the Scope on, and the path to the Scope object at that address.

The .scope file should be installed in the folder of the Scope's parent Lens. So, if this Scope was for the Music Lens, it would be installed into /usr/share/unity/lenses/music/lastfm.scope.

It is worth mentioning that the name of the scope file is not as important as a .lens file, but it is useful to use a specific enough name to avoid conflicts with other scopes.

Note: A Remote Scope should install a D-Bus .service file much like a Lens does. This allows auto-activation if it is not already running when the Lens starts.

Using the Scope Object

The Scope object is your conduit to the current state of Unity. The Scopes parent Lens will keep the Scope updated with the current state, and it is the owner of the Scope's job to keep both the results models (normal and global) updates to reflect this state.

Creating & Initialising the Scope

   1 {
   2   // When creating a Scope object, only the dbus path that you
   3   // would like it exported at is required. This should match
   4   // the DBusPath property in the .scope file if this a remote
   5   // scope. Otherwise any, valid, D-Bus path will do.
   6   this.scope = new Unity.Scope( ("/net/launchpad/scope/lastfm");
   7 
   8   // You can choose at a per-scope level if you want to be
   9   // involved in global searches. This means you can opt-out
  10   // of global searches even if your parent Lens has opted-in.
  11   scope.search_in_global = true;
  12 
  13   // The key thing that you are doing with the Scope object is
  14   // listening for, and reacting to, any state changes.
  15   // Therefore it is essential to connect up to all the important
  16   // signals and property-change events.
  17   // NOTE: The handlers are explained in the specific sections below.
  18 
  19   // First up, we want to be notified whenever the user changes the
  20   // search string in our Lens in the Dash
  21   scope.notify["active-search"].connect(on_search_changed);
  22 
  23   // Next, we want to react to changes when the user searches from
  24   // the home screen in the Dash
  25   scope.notify["active-global-search"].connect(on_global_search_changed);
  26 
  27   // If the user twiddles any of the filters, we want to update our
  28   // search to reflect the change
  29   scope.filters_changed.connect(on_filters_changed);
  30 
  31   // Finally, we want the user to be able to actually click on a
  32   // result and have something happen, so we connect to the
  33   // activate-uri signal, allowing us to react to the click and
  34   // inform Unity of the result.
  35   scope.activate_uri.connect(on_uri_activated);
  36 
  37   // If this is a remote Scope, we'll export it, otherwise for a Local
  38   // Scope, this is where we would use the lens.add_local_scope to let
  39   // the Lens know it exists.
  40   scope.export();
  41 }

Reacting to the active-search changing

When the active-search changes, you can query the current Unity.LensSearch of the Scope via the active_search property. From that object, you can query the current search term and use it to refresh your search results:

   1 private void on_search_changed(Object obj, ParamSpec pspec)
   2 {
   3   Unity.LensSearch? search = scope.active_search;
   4 
   5   if (!search)
   6     return;
   7 
   8   debug("The current search term is: %s", search.search_string);
   9   update_search(search);
  10 }

Reacting to the active-global-search changing

This works much the same as reacting to active-search, but with change of property names:

   1 private void on_search_changed(Object obj, ParamSpec pspec)
   2 {
   3   Unity.LensSearch? search = scope.active_global_search;
   4 
   5   if (!search)
   6     return;
   7 
   8   debug("The current global search term is: %s", search.search_string);
   9   update_global_search(search);
  10 }

The magic "" search term (the Default View)

In the world of the Dash, receiving a search term that equals "" means that you should populate your results model with default results. These are the results that the user sees when they switch to a Lens in the Dash but do not search for anything.

For example, the Applications Lens, when given a search term of "", will populate it's three Categories with the most recently used applications, all installed applications, and a selection of available, highly rated, applications.

You should always strive to have something useful in the default view.

Reacting to the filters-changed signal

Depending on how the content you are searching takes into account Filters, which could be that you need to update a setting on a library in direct response to the filters changing or, in the opposite case, you just need to take them into account when you perform the search (i.e. add them to the end of a URL), how you react to the filters changing will differ.

For the latter, and more general, case, you would chain the filters-changed signal to the update_search callback, and let it just perform another search (as it will take into account the current filter state).

In the second case you might have an extra step in between to set the options on your datastore before updating the search.

NOTE: Although you might feel it's better to react to the filters-changed signal by analysing your current result set and just removing results that don't fit, unless the checking is very simple, it might just be better to do a new query.

Querying Filter State

A Scope is expected to respect the Filters of it's parent Lens. This means that the Scope should know the ids of the Filters and their options so it can understand the state (Note: There is no easy way to do this outside of reading the Lens' source currently, a tool is in the works to simplify the process).

Although you can iterate through the list of Filters the Scope has, it's often easier/makes more sense to query the filter directly and then query it's state:

All examples reference those used in the Lens "Adding Filters" section above

Querying CheckOption State

   1 {
   2   // Get the type CheckOption Filter from the Scope
   3   var filter = scope.get_filter("type") as CheckOptionFilter;
   4  
   5   // As we can have none, one or many options enabled, we need to iterate
   6   // through and see which ones are
   7   foreach (Unity.FilterOption option in filter.options)
   8   {
   9     if (option.active)
  10     {
  11       // We are pretending that our websource uses exactly the same naming as
  12       // the ids our Lens uses. Ahh, to write pretend software...
  13       if (url == "")
  14         url="&type=" + option.id;
  15       else
  16         url+="+" + option.id;
  17     }
  18   }
  19 }

Querying RadioOption State

   1 {
   2   // Get the last-modified RadioOption Filter from the Scope
   3   var filter = scope.get_filter("last-modified") as RadioOptionFilter;
   4 
   5   // Radio option is way simpler, we just need to query the active option
   6   // (if one exists) and do something for that
   7   if (filter.get_active_option())
   8     url += "&modified=" + filter.get_active_option().id;
   9 }

Querying MultiRange State

   1 {
   2   // Get the size MultiRange Filter from the Scope
   3   var filter = scope.get_filter("size") as MultiRangeFilter;
   4 
   5   // As MultiRange works by having an option at the first and last positions,
   6   // we just need to make sure it has a valid FilterOption for both the first
   7   //  and last positions and use that info to sent the range we want.
   8   if (filter.get_first_active() && filter.get_last_active())
   9   {
  10     url += "&min-size=" + filter.get_first_active().id + "&max-size=" + filter.get_last_active().id;
  11   }
  12 }

Querying Ratings State

   1 {
   2   // Get the ratings Ratings Filter from the Scope
   3   var filter = scope.get_filter("ratings") as RatingsFilter;
   4   url += "&min-rating=" + "%f".printf(filter.rating);
   5 }

Handling Activation

Full example

Unity/Lenses (last edited 2013-11-25 15:37:55 by dholbach)