Tutorial: Interactive Script Actions

Table of Contents

 

Introduction

Script actions are scripts that add an entry to a menu and/or tool bar and that can handle user interactions. Scripts actions stay active until terminated by the user or until they self-terminate. 

Events

As soon as the script action is started it handles various events until it is terminated. An event is something that occurs if something is happening. For example if the script action is started, beginEvent is called. If the user clicks an entity an pickEntity event is triggered, if the user clicks a coordinate, a pickCoordinate event occurs, etc.

The minimal structure of a script action is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
include("scripts/EAction.js");

function ExMyMinimal(guiAction) {
    EAction.call(this, guiAction);
}

ExMyMinimal.prototype = new EAction();

ExMyMinimal.init = function(basePath) {
    var action = new RGuiAction(qsTr("&Minimal Example"), RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/ExMyMinimal.js");
    action.setGroupSortOrder(100000);
    action.setSortOrder(0);
    action.setWidgetNames(["ExamplesMenu"]);
};

This example script adds a menu to the bottom of the menu Misc > Examples. The menu text is "Minimal Example".

Note that in order for the script to be found, the file name needs to match the class name, i.e. "ExMyMinimal.js" in this case. It also needs to reside inside a directory with the same name "ExMyMinimal", so this script can for example be put into scripts/Misc/ExMyMinimal/ExMyMinimal.js.

Adding beginEvent

The script above is fully functional and can be triggered. However, it doesn't actually do anything when triggered. Moreover, once triggered, the script stays active until the user terminates it by clicking the right mouse button. To change this, let's implement beginEvent to print something to the command line history of QCAD and terminate the action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
include("scripts/EAction.js");

function ExMyMinimal(guiAction) {
    EAction.call(this, guiAction);
}

ExMyMinimal.prototype = new EAction();

ExMyMinimal.prototype.beginEvent = function() {
    EAction.prototype.beginEvent.call(this);

    EAction.handleUserMessage("Hello World!");

    this.terminate();
};

ExMyMinimal.init = function(basePath) {
    var action = new RGuiAction(qsTr("&Minimal Example"), RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/ExMyMinimal.js");
    action.setGroupSortOrder(100000);
    action.setSortOrder(0);
    action.setWidgetNames(["ExamplesMenu"]);
};

If the tool Misc > Examples > Minimal Example is now started, it prints "Hello World!" to the command line history (line 12) and then terminates (line 14).

If a script does not require any user interaction, such a script can be used to add a menu that does something and then terminates. Examples for such actions are View > Auto Zoom, Select > Select All, Edit > Delete, etc.

Adding Interaction

As soon as a script requires any kind of user interaction, we need to implement more event handlers and tell the script the user needs to do next (e.g. picking an entity or defining a coordinate). In the next step, we enter a state in which the action expects a coordinate from the user. We then draw a circle at every position the user clicks or enters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
include("scripts/EAction.js");

function ExMyMinimal(guiAction) {
    EAction.call(this, guiAction);

    this.pos = undefined;
}

ExMyMinimal.prototype = new EAction();

ExMyMinimal.prototype.beginEvent = function() {
    EAction.prototype.beginEvent.call(this);

    var di = this.getDocumentInterface();
    di.setClickMode(RAction.PickCoordinate);
};

ExMyMinimal.prototype.pickCoordinate = function(event, preview) {
    this.pos = event.getModelPosition();

    if (preview) {
        this.updatePreview();
    }
    else {
        this.applyOperation();
    }
};

ExMyMinimal.prototype.getOperation = function(preview) {
    var doc = this.getDocument();

    var op = new RAddObjectOperation();
    var circle = new RCircle(this.pos, 1);
    op.addObject(shapeToEntity(doc, circle));
    return op;
};

ExMyMinimal.init = function(basePath) {
    var action = new RGuiAction(qsTr("&Minimal Example"), RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/ExMyMinimal.js");
    action.setGroupSortOrder(100000);
    action.setSortOrder(0);
    action.setWidgetNames(["ExamplesMenu"]);
};

In the beginEvent, we no longer terminate the action immediately, but let it run until the user terminates it (right click or Escape). We then implement pickCoordinate to store the position of the mouse cursor or the coordinate entered and to either update the preview or apply the operation (i.e. add the circle). pickCoordinate is called whenever the user moves the mouse to display a preview of the operation that is planned. When the user clicks or enters a coordinate, it is called with parameter preview set to false to indicate that a definitive coordinate has been picked or entered.

updatePreview on line 22 previews the operation returned by getOperation while applyOperation on line 25 actually applies the operation to our document.

getOperation must be implemented to return the operation to preview or apply to the document. This is slightly more complex than what we have seen in the simple API above. This is because a single operation can be used to add multiple objects, modify objects or delete objects.

Adding Widgets to the Options Toolbar 

The circle drawn in our example always has a radius of 1 drawing unit (see line 33). In a next step, we want to allow the user to enter a radius for the circle. QCAD usually uses the options toolbar at the top to display and change such tool parameters. For this, we need to define what widgets we want to show in the options toolbar and what parameters they control. This can be done with a UI file, an XML file that defines a widget and its contents. UI files can be comfortably designed using a software called Qt Designer which comes as part of the Qt toolkit. For this example, we use a simple UI file that can also be created in a text editor (file ExMyMinimal.ui):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>ExMyMinimal</class>
 <widget class="QWidget" name="ExMyMinimal">
  <layout class="QHBoxLayout">
   <item>
    <widget class="QLabel" name="RadiusLabel">
     <property name="text">
      <string>&amp;Radius:</string>
     </property>
     <property name="buddy">
      <cstring>Radius</cstring>
     </property>
    </widget>
   </item>
   <item>
    <widget class="RMathLineEdit" name="Radius">
     <property name="text">
      <string notr="true">1</string>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <customwidgets>
  <customwidget>
   <class>RMathLineEdit</class>
   <extends>QLineEdit</extends>
   <header>RMathLineEdit.h</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

The UI file defined two widgets: a label (QLabel) and a line edit (RMathLineEdit). Important is the name of the line edit ("Radius"). The widget is automatically linked to our script through this name. All we have to do in our script is to define which UI file we want to use (line 9) and to implement a new event handler called slotRadiusChanged, that's "slot" + [the name of our line edit] + "Changed" (line 45):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
include("scripts/EAction.js");

function ExMyMinimal(guiAction) {
    EAction.call(this, guiAction);

    this.pos = undefined;
    this.radius = undefined;

    this.setUiOptions("ExMyMinimal.ui");
}

ExMyMinimal.prototype = new EAction();

ExMyMinimal.prototype.beginEvent = function() {
    EAction.prototype.beginEvent.call(this);

    var di = this.getDocumentInterface();
    di.setClickMode(RAction.PickCoordinate);
};

ExMyMinimal.prototype.pickCoordinate = function(event, preview) {
    this.pos = event.getModelPosition();

    if (preview) {
        this.updatePreview();
    }
    else {
        this.applyOperation();
    }
};

ExMyMinimal.prototype.getOperation = function(preview) {
    if (isNull(this.pos) || isNull(this.radius)) {
        return undefined;
    }

    var doc = this.getDocument();

    var op = new RAddObjectOperation();
    var circle = new RCircle(this.pos, this.radius);
    op.addObject(shapeToEntity(doc, circle));
    return op;
};

ExMyMinimal.prototype.slotRadiusChanged = function(v) {
    this.radius = v;
    this.updatePreview();
};

ExMyMinimal.init = function(basePath) {
    var action = new RGuiAction(qsTr("&Minimal Example"), RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/ExMyMinimal.js");
    action.setGroupSortOrder(100000);
    action.setSortOrder(0);
    action.setWidgetNames(["ExamplesMenu"]);
};

This new function slotRadiusChanged is called whenever the user enters a new radius. It sets the member variable this.radius which is in turn used when creating the circle in getOperation

All scripts in QCAD are based on one of these concepts outlined in this tutorial.

Since every tool in QCAD is implemented as script on the top level, there are plenty of example scripts available. You can find them in our git repository.