Wednesday, January 4, 2017

Scratch-Qt Snap with Ubuntu-App-Platform

Over the 2016 end-of-year holidays, I scratched an itch to write a Qt app that exercises a few capabilities.

My goals were to make:
  • QML app front end with a "Job" component. This has a button and a rectangle that rotates on animation.
  • Tap a "Job" button, and the associated C++ back end method runs in a dedicated thread (so the Qt GUI is not blocked)
  • While the Job's back end code runs, the QML animation is active (a rectangle next to the button goes from orange to purple and rotates), so the user knows that job is in process
  • When the back end code is done, it emits a signal that is heard and stops the animation: the rectangle turns back to orange and stops rotating), so the user knows the job is done
  • And, the snap obtains access to Qt and Ubuntu app libs provided by the Ubuntu-app-platform snap through the Content sharing interface. 
So, this approach would support a dashboard with lots of buttons, each of which is associated with C++ code, with a non-blocking GUI.

The app has other bits, including:
  • A simple shell (enter a command, see the result), which also has buttons to show Snap execution env
  • An oxide browser page
Nothing brand new here, just putting the pieces together for my own enjoyment :).

Let's look at some highlights.

Using the ubuntu-app-platform

As is well known now, the great thing about the ubuntu-app-platform snap is that it provides the libs (Qt/Ubuntu) needed to write an Ubuntu desktop app. Install this snap once, and use it in your app snaps through the Content Sharing interface (thus sharing disk space).

A few steps are needed.


We use the desktop-ubuntu-app-platform remote part (to pull in the ubuntu-app-platform build stuff). You can see the formal description and instructions for using this with:

snapcraft define desktop-ubuntu-app-platform

(You may need to run snapcraft update first.)

Here are the modifications you need in your snapcraft.yaml.

Define the platform plug

Define a plug that connects to the ubuntu-app-platform snap through the content interface like this:

        interface: content
        content: ubuntu-app-platform1
        target: ubuntu-app-platform

This plug:
  • enables the snap to be connected to the "platform" interface
  • to access the platform slot's "ubuntu-app-platform1" content
  • by bind mounting (on interface connection) that content to the "ubuntu-app-platform" directory in my snap 

Use the platform plug

The apps section must also include platform in the list of plugs (interfaces) it uses:

    command: desktop-launch scratch-qt
    plugs: [platform, unity7, opengl, network-bind, gsettings, browser-support, pulseaudio]

Build your application part AFTER desktop-ubuntu-ap-platform

The application is a QML/C++ project that also imports Ubuntu.Components. Naturally, these libs need to be fetched during the snapcraft build process before the application that needs them is built. Therefore, you can use the convenient after: keyword to ensure the proper sequence, as follows:

        after: [desktop-ubuntu-app-platform]

CMake: make ubuntu-app-platform dir

The snap needs a directory named "ubuntu-app-platform" for the bind-mounting provided by the ubuntu-app-platform interface. I create this in CMake:

file(MAKE_DIRECTORY ubuntu-app-platform)
install(DIRECTORY "ubuntu-app-platform" DESTINATION ${DATA_DIR})

As a result, the directory is created during snapcraft in the parts/application/install directory:

$ ls parts/application/install/
components  graphics  lib  qml  ubuntu-app-platform

(My "application" part uses cmake plugin to build/install my qml/C++ app.)

Connect to the interface

The script builds (to prime stage), installs (via snap try prime), connects the interfaces, and launches the snap. The key  here is the interface connection:

snap connect scratch-qt:platform ubuntu-app-platform:platform

Update environment vars in wrapper

This snap has a wrapper script to launch qmlscene and the app.

Various environment variables need to be modified and exported to use the ubuntu-app-platform libs, including: LD_LIBRARY_PATH, QT_PLUGIN_PATH and QML2_IMPORT_PATH.

Non-blocking GUI

A key goal was to make it easy to run the C++ method associated with each  button tap without blocking the GUI, and allowing multiple C++ back-ends to run concurrently. Without implementing threads for this and moving these threads off the default GUI thread, the Qt GUI blocks until the back end processing associated with each button is completed.

Here's my approach.

I created a QML item (Job), a C++ class (Job), and registered the Job class into QML as part of my Scratchqt plugin. The Job displays as a button and a rectangle that rotates and changes color when the job is executing.

When you add a Job item, you specify its "job" string property. When you tap the Job button, it executes the JavaScript runMe() function, which starts the animation (indicating the work is in progress) and then calls the C++ Job::start(job) method.
 Job::start(job) creates an instance of the JobController class (constructed with the job so it knows which job to execute) and then emits Jobcontroller::operate signal. This operate signal is connected to the particular code to run, as explained below.

The JobController constructor creates a Jobs object on the heap.

Jobs class simply contains a method for each Job, for example: job1(), job2(), etc. These methods are where you put the back-end code to run on the particular Job button tap.

Jobs also has a jobDone signal. This is connected to Job::jobDone, as explained later.

The JobController object has a QThread. This is where the back-end execution occurs. The Jobs object is moved to the new QThread. This enables the QML GUI to remain responsive after the C++ back-end execution gets going.

JobController has an if/then/else construct to connect the "operate" signal (emitted by each Job:;start(job)) to the right Jobs slot (Jobs::job1, for example), depending on which Job it is (using the "job" that originates in the Jobs QML item). And then the QThread is started.

Lastly, JobController always connects the Jobs::jobDone signal to the Job::jobDone signal. This allows communication back into the QML so that when the particular back-end job is completed, the animation is turned off (via onJobDone).

Adding an independent Job

Add a new Job item in ThreadController.qml and adjust the values appropriately.

    Job {
        id: job5
        job: "job5"
        button_text: "Job 5" job4.bottom

In the Jobs object jobs.h, add a new slot for your new job5 code:

public slots:
    void job1();
    void job2();
    void job3();
    void job4();
    void job5();

And in jobs.cpp, add the the method:

void Jobs::job5()
    qDebug() << "==== in Jobs::job5()";
    long sleepTime = 3000000;
    Q_EMIT jobDone();

In JobController.cpp constructor, add the if/then/else stanza to connect the operate signal to job5:

    else if (job_name == "job5")
        connect(this, &JobController::operate, jobs, &Jobs::job5);

And here's the new job5 running (so is job3):


1 comment: