Skip to content

Instantly share code, notes, and snippets.

@Dev-iL
Last active November 1, 2020 21:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Dev-iL/7bd222efb01b3bf9fc0ed5d5645d53e7 to your computer and use it in GitHub Desktop.
Save Dev-iL/7bd222efb01b3bf9fc0ed5d5645d53e7 to your computer and use it in GitHub Desktop.
Hijacking uicontrols to do our bidding...

In this example I demonstrate how we can respond to JS events within MATLAB. Here's a preview of what is possible using this approach:

demoNum = 4

Take a look at the code below, which was tested on R2018a. Please save all files in the same folder, and run it using jsEventDemo(demoNum), where demoNum is 1...4.

function varargout = jsEventDemo(demoNum)
%% Handle inputs and outputs
if ~nargin
  demoNum = 4;
end
if ~nargout
  varargout = {};
end
%% Create a simple figure:
hFig = uifigure('Position',[680,680,330,240],'Resize','off');
hTA = uitextarea(hFig, 'Value', 'Psst... Come here...!','Editable','off');
[hWin,idTA] = mlapptools.getWebElements(hTA);
% Open in browser (DEBUG):
% mlapptools.waitForFigureReady(hFig); mlapptools.unlockUIFig(hFig); pause(1);
% web(hWin.URL,'-browser')
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
switch demoNum
%% Demo #1: Respond to mouse events, inside JS, using "onSomething" bindings:
case 1
% Example from: 
% https://dojotoolkit.org/documentation/tutorials/1.10/events/#dom-events
jsCommand = sprintf(fileread('jsDemo1.js'), idTA.ID_val);
%{
require(["dojo/on", "dojo/dom", "dojo/dom-style", "dojo/mouse"],
    function(on, dom, domStyle, mouse) {
       var myDiv = dom.byId("%s");

       on(myDiv, mouse.enter, function(evt){
           domStyle.set(myDiv, "backgroundColor", "red");
       });

       on(myDiv, mouse.leave, function(evt){
           domStyle.set(myDiv, "backgroundColor", "");
       });
});
%}
hWin.executeJS(jsCommand);

%% Demo #2: Respond to mouse click events, inside JS, using pub/sub:
case 2
% Example from: 
% https://dojotoolkit.org/documentation/tutorials/1.10/events/#publish-subscribe
hTA.Value = "Click here and see what happens";
jsCommand = sprintf(fileread('jsDemo2.js'), idTA.ID_val);
%{
require(["dojo/on", "dojo/topic", "dojo/dom"],
    function(on, topic, dom) {

        var myDiv = dom.byId("%s");

        on(myDiv, "click", function() {
            topic.publish("alertUser", "Your click was converted into an alert!");
        });

        topic.subscribe("alertUser", function(text){
            alert(text);
        });
});
%}
hWin.executeJS(jsCommand);

%% Demo #3: Trigger MATLAB callbacks programmatically from JS by "pressing" a fake button:
case 3
hB = uibutton(hFig, 'Visible', 'off', 'Position', [0 0 0 0], ...
              'ButtonPushedFcn', @fakeButtonCallback);
[~,idB] = mlapptools.getWebElements(hB);
jsCommand = sprintf(fileread('jsDemo3.js'), idTA.ID_val, idB.ID_val);
%{
require(["dojo/on", "dojo/dom", "dojo/dom-style", "dojo/mouse"],
    function(on, dom, domStyle, mouse) {
       var myDiv = dom.byId("%s");
       var fakeButton = dom.byId("%s");

       on(myDiv, mouse.enter, function(evt){
           fakeButton.click();
       });
});
%}
hWin.executeJS(jsCommand);

%% Demo 4: Trigger MATLAB callbacks and include a "payload" (i.e. eventData) JSON:
case 4
hB = uibutton(hFig, 'Visible', 'off', 'Position', [0 0 0 0],...
             'ButtonPushedFcn', @(varargin)smartFakeCallback(varargin{:}, hWin));

[~,idB] = mlapptools.getWebElements(hB);
jsCommand = sprintf(fileread('jsDemo4.js'), idTA.ID_val, idB.ID_val);
%{
var payload = [];
require(["dojo/on", "dojo/dom", "dojo/topic", "dojo/mouse"],
    function(on, dom, topic, mouse) {
       var myDiv = dom.byId("%s");
       var fakeButton = dom.byId("%s");

        topic.subscribe("sendToMATLAB", function(data){
            payload = data;
            fakeButton.click();
        });
       
       on(myDiv, mouse.enter, function(evt){
           data = {action: "enter",
                    coord: [evt.clientX, evt.clientY]};
           topic.publish("sendToMATLAB", data);
       });

       on(myDiv, mouse.leave, function(evt){
           data = {action: "leave",
                    coord: [evt.clientX, evt.clientY]};
           topic.publish("sendToMATLAB", data);
       });
});
%}
hWin.executeJS(jsCommand);
end % switch

% keyboard; %DEBUG
end

function fakeButtonCallback(obj, eventData) %#ok<INUSD>
  disp('Callback activated!');
  pause(2);
  clc;
end

function smartFakeCallback(obj, eventData, hWin)
  % Retrieve and decode payload JSON:
  payload = jsondecode(hWin.executeJS('payload'));
  % Print payload summary to the command window:
  disp("Responding to the fake " + eventData.EventName + ...
       " event with the payload: " + jsonencode(payload) + ".");
  % Update the TextArea
  switch payload.action
    case "enter"
      act_txt = "entered";
    case "leave"
      act_txt = "left";
  end
  coord = payload.coord;
  str = ["Mouse " + act_txt + " from: "; "(" + coord(1) + "," + coord(2) + ")"];  
  obj.Parent.Children(2).Value = str;
end

Several thoughts:

  1. The attached .js files will not work by themselves, rather, they require sprintf to replace the %s with valid widget IDs. Of course, these could be made into proper JS functions.
  2. I tried getting it to work with a <textarea> control, so that we would get the payload right in the callback's eventData object in MATLAB, but couldn't get it to fire programmatically (solutions like this didn't work). This is why I had to store the payload as JSON, and retrieve it with jsondecode(hWin.executeJS('payload')).
require(["dojo/on", "dojo/dom", "dojo/dom-style", "dojo/mouse"],
function(on, dom, domStyle, mouse) {
var myDiv = dom.byId("%s");
on(myDiv, mouse.enter, function(evt){
domStyle.set(myDiv, "backgroundColor", "red");
});
on(myDiv, mouse.leave, function(evt){
domStyle.set(myDiv, "backgroundColor", "");
});
});
require(["dojo/on", "dojo/topic", "dojo/dom"],
function(on, topic, dom) {
var myDiv = dom.byId("%s");
on(myDiv, "click", function() {
topic.publish("alertUser", "Your click was converted into an alert!");
});
topic.subscribe("alertUser", function(text){
alert(text);
});
});
require(["dojo/on", "dojo/dom", "dojo/dom-style", "dojo/mouse"],
function(on, dom, domStyle, mouse) {
var myDiv = dom.byId("%s");
var fakeButton = dom.byId("%s");
on(myDiv, mouse.enter, function(evt){
fakeButton.click();
});
});
var payload = [];
require(["dojo/on", "dojo/dom", "dojo/topic", "dojo/mouse"],
function(on, dom, topic, mouse) {
var myDiv = dom.byId("%s");
var fakeButton = dom.byId("%s");
topic.subscribe("sendToMATLAB", function(data){
payload = data;
fakeButton.click();
});
on(myDiv, mouse.enter, function(evt){
data = {action: "enter",
coord: [evt.clientX, evt.clientY]};
topic.publish("sendToMATLAB", data);
});
on(myDiv, mouse.leave, function(evt){
data = {action: "leave",
coord: [evt.clientX, evt.clientY]};
topic.publish("sendToMATLAB", data);
});
});
@xiangruili
Copy link

When a property, like backgroundColor in jsDemo1, is changed in JS, the visual effect is immediate. Apparently Matlab caches the property value, as 'hTA.BackgroundColor' is still [1 1 1]. Is there any way to force Matlab to update the cached property value? If so, this may be a way to transfer data from JS to Matlab.

@Dev-iL
Copy link
Author

Dev-iL commented Nov 1, 2020

@xiangruili Good thinking! When I was investigating this in the distant past, it resulted in the button trick you're already aware of. If I had to come up with a different solution today, I would probably set up a server on the MATLAB side and load some js library to send messages to this server whenever I want to pass some information along. It's either that or reverse-engineering MATLAB's communication protocol with uifigures, which seems like a harder task...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment