UI Dialogue/Form Scripts

Parent Previous Next

(All of this section is a work in progress!)

June  22nd 2011


This is based on just making a few forms for TeXworks' Qt Scripts, web gleanings, the Qt site and various forums, and TeXworks specific information received (gratefully) from Jonathan Kew and Stefan Löffler.


Forms / Dialogues


Introduction —


(Example below - Putting it all together)


References here to objects and functions commencing twPan refer to a wrapper object (not required to work with Qt forms) helper_twPan.mod described a bit at present in User 'Library' Modules and helper objects


Qt Creator for Form Design


Qt provides a basically free WYSIWYG User Interface editor called Qt Creator http://qt.nokia.com/products/developer-tools/, that can be used to design forms / dialogues (saved as an xml file with .ui extension) that can be linked to your Qt .js scripts at Script run time.


You can start off directly from File/New and save your form into your TexWorks script sub-directory.



You can also make your Script File in Version 1 series of Qt Creator, if not using TeXworks,

or another scripting  editor (use Qt Script File above).





In 2.1 series of Qt Creator the same thing now looks like this,

and possibly no direct provision is now made for Qt Script Files.


In your TeXworks script, you can read some properties, connect to some events (signals), and call some functions (slots) of the Qt objects contained in your .ui form(s).  It is  a bit hit and miss to find out what works, as there does not yet appear to be a definitive Qt list of what parts, of which Qt standard widgets, are scriptable at present. Words in Qt documentation like "public" don't necessarily mean that what is being referred to is available to script.


There is no way of avoiding reading various pieces of Qt documentation and becoming familiar with Qt's terms and vocabulary, it is actually quite well written in a fairly consistent manner, and is well worth the effort.


To use form objects (widgets), you need to connect in script to the form's objects, and also separately make connections to any of the form's events (signals) that you want to respond to, like .clicked  .textChanged  and so on. Later, before the script  finalises, you need to "disconnect" from these "signals".  This is done in the script editor, and executed at script run time, and not at the moment in the Qt Creator form editor.


You can also use the Qt script debugger to help, there is a video here http://www.youtube.com/watch?v=pyUKtOV9qn8.


Detail


In script, when you create your form (dialogue), you assign the form to a QtScript variable.


Later when you have finished with the form, before your Script closes, you must mark it for removal from memory.


You do this by calling a Qt QDialog function, deleteLater() (see: http://tug.org/mailman/htdig/texworks/2011q2/004428.html)


  var myDialogue = TW.createUI(__FILE__.replace(".js",".ui"));

                 // or form an xml string  TW.createUIFromString(QString)


// do some stuff



// when dialogue is no longer required at all, but in any event before script finishes ...


myDialogue.deleteLater();

In the above example, the form's xml is in a file by the same name, in the same folder, as the script that is going to use it, but with a .ui extension instead of a ,js extension.

So if you use this naming method, and have say myScript.js and myScript,ui in the same directory,  then make yourself a helper script to open the Qt Creator from inside your .js while you are editing it in TeXworks. For example here is a Windows standard while using a standard installation of Qt Creator 1.2  activated by, Alt(OQC) (change as needed) —


// TeXworksScript

// Title: Open Qt Creator on File

// Description: Opens Qt dialogue builder using current Script's Name

// Author: Paul Norman

// Version: 0.1

// Date: 2011-07-23

// Script-Type: standalone

// Context: TeXDocument

// Shortcut: Alt+O, Alt+Q, Alt+C


eval(TW.app.getGlobal("helper_twPan")); // Comment if NOT Needed - This includes PhpJs ($P), twConst, msgBox, twPan ($tw)


var uiFileName = TW.target.selection;


if (uiFileName === "")

{

//dialogue .ui name is in same directory as script being edited, and has same stem name--


uiFileName = TW.target.fileName.replace(".js",".ui") ;

}

else

{

//dialogue .ui name is in same directory as script being edited, and is highlighted in Tw editor as in:

// showMoreDialogue = TW.createUI(scriptPath + "showMore.ui");


       uiFileName = TW.target.fileName.substr(0, TW.target.fileName.lastIndexOf("/") +1) + uiFileName;

}


var retVal = TW.system("cmd /c start \"G:/Qt/qtcreator-2.1.0/bin/qtcreator.exe\" \"" + uiFileName +"\"");


retVal = null;


As you might write code that uses User input to create a form (see below) in a function using one of createUIFromString(QString,QWidget*) or more generally createUIFromString(QString), and need to call it later, it may be worth adopting a set approach to the whole thing and always start by declaring all the variable names you are going to name forms by, at the top of you script (in global space), and put them in an array at the same time.


Then later either at the end of your main run before the Script closes, iterate through the array, and test for whether the variable was ever created as a form, and if so call deleteLater() on it.  


Perhapos even create and hide (as shown below here) all your dialogues at once at the beginning of your script.  There are other possibilities but this looks for the moment to be fail safe, with the draw back only  of a one time unprofessional flicker when created and hidden.


  var myFirstForm = null; // main form

  var mySecondForm = null;

  var myThirdForm = null;


  var myForms = [];


// any time later or straight a way


  myFirstForm = TW.createUI("a/path/to/myFirstForm.ui");

  myForms.push(myFirstForm);


You can then hide it straight away if you wish.


mySecondForm = TW.createUI("a/path/to/mySecondForm.ui");

myForms.push(mySecondForm);


 mySecondForm.setVisible(false); // there will be a momentary flicker


// And so on.



While the script is running, any dialogue/form that has been created and has not already been .deleteLater() -ed can be called (again) using  


  var  answer = myForm.exec();


And it will show modally (see below).


Then at the end of the script run something like:—

 

for (var x = myForms.length -1; x > -1; x--)

{ // done reverse to destroy main form last

     {

           if (myForms[x] !== null)

            {

             myForms[x].deleteLater();

            }          

      }


"Finished"


There is a signal "finished" emitted by the form when it closes.


If you consider the first form that you show when the script runs your 'main' form (i.e. it is around for the whole of you script while other forms/dialogues may come and go),

you could connect into that signal as part of UIconnectVarious() (below UIconnectVarious("connect"|"disconnect"))


eval("main_form.finished." + connectAction + "(main_form_finished)");


and put your end of script (finalization) stuff in the function you connect to that signal.


  function main_form_finished(response)

      {

 

           // response will be as shown below: Cancel, or Ok  0, or 1


          UIconnectVarious("disconnect") // see below UIconnectVarious("connect"|"disconnect")

       

          //  save settings to disk or do anything else need at the end of the script


   for (var x = myForms.length -1; x > -1; x--)

{ // done reverse to destroy main form last

     {

           if (myForms[x] !== null)

            {

             myForms[x].deleteLater();

            }          

      }


How ever you do it,  it needs to be done!


Once Created


Once created, you can then use dot notation to walk through the form's objects. As an object's immediate parent is related to the design object (widget) it is on, this can make the dot notation method a bit complicated.



  myDialogue.niceLookingPanel.NiceFrame.MyObject.text = "Hello";


There are at present  some widgets that you have to completely  build in the Qt form Creator, to which you can not add items  later, the QComboBox is one example.  You can however use a function in your script to pre-load the .ui xml file that will build the form (as plain text), reprocess it, and in script pass the altered xml to a top level TW function as a string -  


 var myDialogue =  TW.createUIFromString("<My alterd> <XML / ...>";


The string may be contained in a script variable or script function that you write. You can write a whole form this way from scratch (just look at a simple form's disk file in a text editor to get the formats required).


As said before, .dot paths like:  myDialogue.niceLookingPanel.NiceFrame.MyObject.text = "Hello";   can be hard to keep track of.


A simple method is to simply assign the same names in script, to the form's objects' names, that you want to work with.



When scripting a 'widget' (form object), google "Qt QPushButton" for example,

and/or go directly to  http://doc.qt.nokia.com/4.7/index.html and

pages like http://doc.qt.nokia.com/4.7/qpushbutton.html for more programming information.


There is a top level  function in TwScript that can find form objects, TW.findChildWidget... and obtain references for scripting to the form's objects directly, ignoring how they are positioned on the form. You pass the Var name (as a script object) that you gave the form in script when you created it (this can be different from the form's own name in Qt Creator, which is 'dialog' by default), and the name of the widget you are trying to find as a string (examples below).


There are a number of approaches of exposing the form's objects, that you need, to script. Here is one simple method.


Create an array of object names (as strings), and then iterate through them, assigning the names as objects in script. This requires that the widgets (labels, textboxes etc ..) on the form have the same names as you intend to use for them in script, although you can creatively devise strategies to alter this requirement.


Here is a direct approach I use, if for no other reason than it keeps all the names I want in front of me in the script editor, while scripting.


Only enter the names of form objects that you need.


 var scriptWidgets = ["showImageInformation", "radioPerrcentage", "radioWidth", "radioHeight",

                                      "editSize",  "editDpi", "checkSaveDirectory",

                                  "plainTextEditConvertSettings", "btnGetAnotherImage", ... etc

                                 ];


Then later after creating/loading the form :—


for (widget in scriptWidgets)

{

// Define required  dialogue .ui widgets here in QtScript as programmable objects


eval("var " + scriptWidgets[widget] + " = TW.findChildWidget(myDialogue,\"" + scriptWidgets[widget] + "\")");


}


For scope reasons, this needs to be done at top level, and not in a function call.


You can then directly refer to a "found" form object's properties and functions (or at least to the ones that Qt exposes by default to scripting).


myDisplayAtrea.setHtml("<span style=\"color:navy; background-color:rgb(20,45,77)\">Hi TeXworks User</span>");



myLabel.text = "Show This";


myLabel.text = "Show <span style=\"font-weight:bold\">This</span>"; //


To show HTML based text, you'll need make sure that the textFormat of the widget is set either to AutoText (detection) or explicitly to RichText.


Qt uses a subset of HTML to show rich text, see http://doc.qt.nokia.com/latest/richtext-html-subset.html






When designing a Qt form or dialogue, you'll need to get use to using Design Layouts to make

sure your material works and looks right if resized, and on different operating systems

and their versions. See  http://doc.qt.nokia.com/4.7/designer-manual.html

http://doc.qt.nokia.com/latest/designer-layouts.html  

http://doc.qt.nokia.com/4.7-snapshot/widgets-and-layouts.html  and

http://doc.qt.nokia.com/latest/layout.html



Once the script's side of  variables  have been created to represent form objects, and linked to in the script's global space, any of the form objects that have events (signals) that you want to 'listen' for, can be "connected" to. While in Qt Creator you can view the name of a widget's possibly available signals (and slots),  in the bottom tab called 'Signals and Slots Editor' (shown above).


Steps:


Choose the Green plus sign + (1. below) .

Then choose your form object (widget) 2.  and then the available signals for it will show in the signals dropdown  3. as shown in the bottom of the green area above.




As said before, widgets (form objects) you are 'listening to', have to be "disconnected" prior to the script finishing. This necessitates that you call the form's (dialogue's) .exec() function after having created the form, to keep the script held running, while the dialogue shows in a modal fashion. Otherwise the script would just run straight through and effectively disconnect the form's objects from User interaction.


So using .exec(),  halts the script's primary linear execution, but allows functions that have been connected to signals in the form, to operate effectively. When the User closes the form, linear script execution continues form the point immediately after .exec(), as shown in the example below Putting it All Together


When the form has .exec() called on it in script, when the User closes the form, it will return one of two values:



Integer Returned

QDialog::Accepted (e.g. OK)

1

QDialog::Rejected (e.g. Cancel)

0


These, Accepted or Rejected  can be set on any form widget addressing the form object itself (default name "dialog" in Slot column) in the Signals and Slots Editor in Qt Creator.


See http://doc.qt.nokia.com/latest/qdialog.html#exec and  http://doc.qt.nokia.com/latest/qdialog.html#DialogCode-enum


To easily  enable initialising and finalisation of events (signals),  a function something like this can be written—

function UIconnectVarious(connectAction)

{

/* This streamlines initialisation and finalisation of the script dialogue

slot connections connectAction passes either 'connect' or 'disconnect'

.slotEvent is something like .clicked

*/

// eval("nameofWidget.slotEvent." + connectAction + "(functionYouWriteInThisFile)");

                //                    functionYouWriteInThisFile - !make function in Global Space!


eval("btnGetAnotherImage.clicked." + connectAction + "(showImage)");


eval("plainTextEditConvertSettings.textChanged." + connectAction + "(plainTextEditConvertSettings_textChanged)");


eval("comboInsertSwitches['currentIndexChanged(int)']." + connectAction + "(comboInsertSwitches_SwitchChosen)");

}


When called, connectAction will be either "connect" and "disconnect", run at the begging of things, and before the script finishes up ( after .exec() see also "Finished").


Events for objects will sometimes return values, but afaik, never the sender object itself. So this in a function connected to a form object's signal, never refers to the form object / widget itself, but to the script's Global this.


If there is more than one possible returned 'thing' for an event (an overload) you need to use the style  marked above  comboInsertSwitches['currentIndexChanged(int)']..

Check the Qt documentation for the object (widget) concerned i.e. http://doc.qt.nokia.com/4.7/qcombobox.html#currentIndexChanged  there you will see that you will either have an integer indicating which item was chosen, or as shown there by the next entry, the text of the item as a QString (in JavaScript casts to a normal string), returned to your function, in this case comboInsertSwitches_SwitchChosen


If you wanted the string option, when available, you would need to write the signal connection like this.


eval("comboInsertSwitches['currentIndexChanged(QString)']." + connectAction + "(comboInsertSwitches_SwitchChosen)"


And then a script function called comboInsertSwitches_SwitchChosen would expect a string, instead of a number. The widget's Qt documentation is the best way to tell what you could expect to find as workable.


This then means that when you write comboInsertSwitches_SwitchChosen  (you make the name up) you can collect the value returned from the Script Engine supplied arguments array, or if you know what you are expecting, directly as in ...


   function comboInsertSwitches_SwitchChosen( index)

              {

                  if (index == 0) { return}

                  }


However, you never put the variable name ( index ) in ...


eval("comboInsertSwitches['currentIndexChanged(int)']." + connectAction + "(comboInsertSwitches_SwitchChosen( index ))");      


Should look like (functionName()  no following parenthesise) :—


eval("comboInsertSwitches['currentIndexChanged(int)']." + connectAction + "(comboInsertSwitches_SwitchChosen)");



You can test what is returned to your function by using the arguments array and iterating over it.


 function comboInsertSwitches_SwitchChosen()

              {                    

                     for (I in arguments)

                         {

                            TW.information(null, "", "Argument Number: "  I + ",   Value: "  + arguments[I]);

                         }

                  }


So far I think that I have never found more than one thing returned. So generally  the following will always work, if you already have researched and found that there will be something you need there.


 function comboInsertSwitches_SwitchChosen(whatEver)

             {


                TW.information(null, "", whatEver);


                anotherFormObject.text = whatEver;


                yetAnotherFormObject.setHtml(whatEver);  

   

              }


Putting it all together ...


A script header could look something like the following


With this kind of script operation it is convenient to place the vast majority of functions, unconventionality, at the end of the script :—


// TeXworksScript

// Title:  Convert Image

// Description: Uses ImageMagick to make image alterations

// Author:  Paul Norman

// Version: 0.1

// Date: 2011-05-06

// Script-Type: standalone

// Context: TeXDocument

// Shortcut: Alt+C, Alt+I



eval(TW.app.getGlobal("helper_twPan")); // Comment if NOT Needed - This includes PhpJs ($P), twConst, msgBox, twPan ($tw), string.toTitleCase()


var convertImage; // dialogue Script Object

var scriptPath = twPan.callingScriptPath(__FILE__);

var scriptDir = scriptPath.substr(0,scriptPath.length -1);

var documentPath = twPan.callingScriptPath(TW.target.fileName);

var ... etc

var myForms = [];


if (twPan.os == "Windows")

{ var lineBreak = "\r\n";

var cmdSeperator = "&";

}

else

{var lineBreak = "\n";

var cmdSeperator = ";";// http://vic.gedris.org/Manual-ShellIntro/1.2/ShellIntro.pdf

}

var scriptWidgets = ["showImageInformation", "radioPerrcentage", "radioWidth", "radioHeight", "editSize",  ... etc}];


function UIconnectVarious(connectAction)

{

/* This streamlines initialisation and finalisation of the script dialogue

slot connections connectAction passes either 'connect' or 'disconect'

.slotEvent is something like .clicked

*/

// eval("nameofWidget.slotEvent." + connectAction + "(functionYouWriteInThisFile)");

// !make function in Global Space!


eval("btnGetAnotherImage.clicked." + connectAction + "(showImage)");

eval("btnConvertUsingSettings.clicked." + connectAction + "(IM_convert)");

eval("btnJustCopyGraphic.clicked." + connectAction + "(JustCopyGraphic)");

... etc

eval("main_form.finished." + connectAction + "(main_form_finished)");


}


 // note above: main_form.finished, when referring to the form use its Script given variable name, not necessarily the one seen in the Qt Creator interface.


  function main_form_finished(response)

      {

  //  put your end of script (finalization) stuff in the function you connect to that signal.

 

           // response will be as shown below: Cancel, or Ok  0, or 1


          UIconnectVarious("disconnect") // see below UIconnectVarious("connect"|"disconnect")

       

          //  save settings or do anything else need at the end of the script


for (var x = myForms.length -1; x > -1; x--)

{ // done reverse to destroy main form last

     {

           if (myForms[x] !== null)

            {

             myForms[x].deleteLater();

            }          

      }


//           --- Script BEGIN ---


// convertImage = TW.createUI(__FILE__.replace(".js",".ui"));

convertImage = TW.createUIFromString(reBuildUi()); // read in QComboBox members in own function reBuildUi();


myForms.push(convertImage);



/* This next loop has to be done in Global space and not in a general function,

for Script widget objects, and `in memory functions_()' generated here,

to be available generally to script.

Done this way as you don't need to know the parent of a widget first

(as in complex layouts).

*/


for (widget in scriptWidgets)

{

// Define required convertImage dialogue .ui widgets here in QtScript as programmable objects


eval("var " + scriptWidgets[widget] + " = TW.findChildWidget(convertImage,\"" + scriptWidgets[widget] + "\")");


}


UIconnectVarious("connect")


// setup procedures (could be in function calls)


showImageInformation.setHtml('<h2 style=\"font-weight:bold\">Please \'Get A Graphic\' to Use ...</h2>');

// labelScriptLogo.text = "<p><img height=\"185\" src=\""+scriptPath+"TwScripting_width100px.png\"></p>";


labelVersion.text = "<p>Convert Image v."

+ twPan.file_get_contents(__FILE__).split("\n")[4].replace("// Version: ","")

+ "</p>" ;


labelIMlocalHelp.text = twPan.file_get_contents(scriptPath + "labelIMlocalHelp.txt");


ans = convertImage.exec();  // this will wait for the dialogue to be closed.


/* You can test ans for an integer

 ans will generally  be one of:



Integer Returned

QDialog::Accepted (e.g. OK)

1

QDialog::Rejected (e.g. Cancel)

0


These can be set on any form widget addressing the form object itself (default name "dialog" in Slot column) in the Signals and Slots Editor in Qt Creator.


*/


UIconnectVarious("disconnect")


//           --- Script  END ---.



...  then  heaps and heaps of functions()  that you write!


    Including functions for the things in the form that you have

    made 'connects' to that the script's User will click on and so on ...



Created with the Personal Edition of HelpNDoc: Free Web Help generator