(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();
}
}
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:
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);
}
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