Scripting: QT signal to named member function connections


#1

While reviewing QtScript documentation I noticed that it’s possible to connect signals using an object and method name.

And it turns out this is actually a long-standing feature of QtScript and already available within Interface – offering us a profound way to simplify connections within scripts.

The basic gist is that…

Instead of doing this:

Something.connect( function(){} )

… we have the option to do this:

Something.connect(thisObject, functionName);

(where functionName can either be a Function or a String used to look up the method on thisObject)

The best part is that the system resolves and binds functionName to thisObject for you – which means instead of dealing with dangling functions or indirect closures, you can simply design handlers in context of your prototype and proceed using normal JavaScript conventions.

When your handlers are later signaled, this approach provides you with automatic access to this (without any of _that fuss).

For reference here are the possible .connect patterns:

Window.domainChanged.connect(Window, 'alert');

//... equivalent to:
Window.domainChanged.connect(Window, Window.alert);

//... equivalent to:
Window.domainChanged.connect(function(domain) {
  Window.alert(domain);
});

//... *not* equivalent to:
Window.domainChanged.connect(Window.alert);

BEFORE (the more uphill way):

(function() {
  var _that = this;

  function _onmsg(channel, message, sender) {
    _that.onmsg(channel, message, sender);
  }

  function cleanup() {
    try { Messages.sendMessage.disconnect(_onmsg); } catch(e) {}
  }

  this.preload = function(uuid) {
    this.uuid = uuid;
    this.lastMessage = "n/a";
    Messages.sendMessage.connect(_onmsg);
  };

  this.unload = function(uuid) {
    print("goodbye from", uuid, this.uuid);
    cleanup();
  };

  this.onmsg = function(channel, message, sender) {
    this.lastMessage = message;
  };

  Script.scriptEnding.connect(cleanup);
})

AFTER (a more downhill way)

(function() {

  this.preload = function(uuid) {
    this.uuid = uuid;
    this.lastMessage = "n/a";
    Messages.sendMessage.connect(this, 'onmsg');
  };

  this.unload = function(uuid) {
    print("goodbye from", uuid, this.uuid);
    try { Messages.sendMessage.disconnect(this, 'onmsg'); } catch(e) {}
  };

  this.onmsg = function(channel, message, sender) {
    this.lastMessage = message;
  };

  Script.scriptEnding.connect(this, 'unload');
})

(note the absence of any var _that = this; closure hacks above – and how instance properties can be accessed intuitively and consistently from within all methods)


###Entity script example:

note: if testing this you won’t see any visible Entity changes until you emit a color message from somewhere else like the Script Console.

(function() {
    this.lastMessage = "n/a";

    this.onmsg = function(channel, message, sender) {
       this.lastMessage = message; // no need for a "_that" closure!                                                                                                                        
       if (channel === 'color')
          Entities.editEntity(this.uuid, { color: JSON.parse(message) });
    };
    //elsewhere:
    //  Messages.sendMessage('color', JSON.stringify({red:0,green:255,blue:0}))                                                                                                          

    this.preload = function(uuid) {
       this.uuid = uuid;
       Messages.subscribe('color');
       Messages.messageReceived.connect(this, 'onmsg');
    };

    this.unload = function(uuid) {
       Messages.messageReceived.disconnect(this, 'onmsg');
       Messages.unsubscribe('color');
    };
 })

// … was going to put more ideas/examples here, but decided to keep things brief for now – if anybody is curious to see more or how this can be applied more generally just let me know.


#2

As a side note: connecting your entity script unload to script ending is not needed and will have no effect. your entity script will be unloaded and have it’s unload method called before the script is ended.


#3

OK – good to know! And to be honest I was hoping for a slightly-wider lens here… :wink:

In the wild the de facto standard for extending HiFi resources (such as Entity or Interface scripts) has essentially become paste driven development… which doesn’t seem super-duper effective or to be leading into many organic opportunities for scripts to become well-documented, maintained and composed into aggregate solutions.

The before/after example you touched upon was attempting to highlight how calling conventions can significantly affect code complexity. And I think the tidier signaling convention is not just of immediate value – it also opens the door for leveraging JavaScript’s prototypical inheritance model across different developers, scripts and domains.

And that seems like a great fit with circumstance – potentially fostering accessibility, code re-use, collaboration, googleability (ie: self-sufficiency), debugging, adaptation, cross-pollination, etc. To me these benefits are worth pursuing and their absence worth recognizing, even if there are no silver bullets.

Below is an example of using JavaScript inheritance across different scripts.

“BaseClass” is the raw Entity script example from my original post, which is extended passively by the subclassing script below.

(function() {

    // 1) BaseClass -- the original, unmodified entity script
    // 2) ... SubClass instanceof BaseClass
    // 3) ...... MySubClass instanceof SubClass
    // 4) ......... *this Entity* instanceof MySubClass

// ---------------------------------------------------------
// 1) BaseClass
    function __get_BaseClass_constructor() {
       var src = "https://cdn.rawgit.com/humbletim/e7621c5a087d345c717f/raw/a8202d1079a342789012a3523dc29cdfc2a75b33/entity.js";
       var BaseClass = $require(src);
       __get_BaseClass_constructor = function() { return BaseClass; };
       return BaseClass;
    }

// ---------------------------------------------------------
// 2) ... SubClass
    function __get_SubClass_constructor() {
       var BaseClass = __get_BaseClass_constructor();
       
       SubClass.prototype = new BaseClass();
       SubClass.prototype.$super = SubClass.prototype;

       return SubClass;

       function SubClass() { // extends BaseClass

          // let's override an existing method from BaseClass...
          this.onmsg = function(channel, message, sender) {
             print("OVERRODE METHOD...", [channel, message, sender]);
             // ... while still leveraging the original!
             return this.$super.onmsg.apply(this, arguments);
          };

          // let's add a new *overridable* utility method ...         
          this.send = function(channel, message) {
             Messages.sendMessage(channel, message);
          };
          
          // let's add some user interaction ...                      
          this.clickReleaseOnEntity = function(uuid, evt) {
             // ... while still leveraging original's properties!
             var question = "Last message was: '"+this.lastMessage+"';"+
                "... shall I transmit a random color for you?";

             if (Window.confirm(question))
                this.send('color', JSON.stringify(this._random_color()));
          };

          // boring helper function (but it's also overridable)
          this._random_color = function() {
             return {
                red: 0xff * Math.random(),
                green: 0xff * Math.random(),
                blue: 0xff * Math.random()
             };
          };

       } //SubClass
    }

    // now let's get to business with *this* Entity script
    this.preload = function(uuid) {

       var SubClass = __get_SubClass_constructor();

       var MOCKSEND = true; // FIXME: set to false once sandbox is upgraded

// 3) ...... MySubClass
       function MySubClass() { // extends SubClass
          if (MOCKSEND) {
             // override SubClass.send...
             this.send = function(channel, message) {
                Messages.messageReceived(channel, message, this.uuid);
             };
          }
       }
       MySubClass.prototype = new SubClass();

       // our current .preload was just to help carry us this far
       delete this.preload;
       
// 4) ......... *this Entity*
       this.__proto__ = new MySubClass();

       {  // VR: virtual reality
          var BaseClass = __get_BaseClass_constructor();
          print("this instanceof SubClass:",   this instanceof SubClass   ); // true!
          print("this instanceof MySubClass:", this instanceof MySubClass ); // true!
          print("this instanceof BaseClass:",  this instanceof BaseClass  ); // true!
       }
       
       // .preload wasn't overriden, so is naturally BaseClass.preload here
       this.preload(uuid);
    };

    // helper function (module system where art thou?)
    function $require(src) {
       var xhr = new XMLHttpRequest();
       xhr.open("GET", src, false);
       xhr.send();
       return eval("1,"+xhr.responseText);
    }
 })```
... ps: here's a screenshot of the above script working:
<img src="/uploads/highfidelity/5041/74a1e58451deba04.png" width="480" height="500">