There are a couple of other threads on this topic, but they say it cannot be done:
- http://www.daz3d.com/forums/discussion/65903/any-way-to-allow-scene-to-be-accessed-when-script-is-open/p1
- http://www.daz3d.com/forums/discussion/comment/657812/#Comment_657812
The trick is to launch the dialog Non-Modally, as described by Rob in the 2nd link above, and follow that with a local event loop.
There are a number of things that need to be taken care of that Modal scripting need not, especially ensuring that the local event loop gets terminated no matter how the dialog gets closed (the technique for this is per this thread). But it is probably no more open to mis-use than a Non-Modal plug-in is.
The attached file NonModalDialog.dsa is a working example, with lots of explanatory comments.
A couple of questions remain:
Q1) Is it a good idea to call sleep() each time thru the event loop? If so, what is the best value to use? (The example uses 50 ms).
Q2) Is it necessary to call gc() each time thru the event loop?
Q3) Is there anything else that needs to be done to ensure that a Non-Modal script behaves nicely?
Here's the example code in-line:
// NonModalDialog.dsa
// DAZ Script: Non-Modal Dialog tester and example.
// Revised 23-Apr-2016
// Created 15-Apr-2016
// Written by Praxis.
// Tested in DAZ Studio Version 4.8.0.59 Pro Edition (64-bit) under Windows7 Pro.
//****************************************************************************
//
// In the thread titled "how to make a modalless dialog?" of August 2014, rbtwhiz replied:
// | DzBasicDialog inherits DzDialog, which is a thin wrapper around QDialog.
// | Among other things, DzDialog exposes QDialog::exec(),
// | which ignores any modality setting when it calls QWidget::show().
// | DzDialog inherits DzWidget,
// | which provides a getWidget() method that will return the wrapped QWidget.
// | Because you can access properties, public slots and signals of a QObject in script...
// | you can actually set QDialog::modal to true and call QWidget::show(), and it will show... momentarily.
// | The problem is, show is non-blocking... so the script continues on to the next statement and ultimately finishes.
// | To avoid memory leaks, all DzWidget derived objects have "ScriptOwnership",
// | meaning they are owned by the script environment and are garbage collected when the script goes out of scope...
// | i.e. when the script finishes or crashes.
// | So, while you can actually create and show a modeless dialog in script,
// | the script will finish and garbage collect it before you have a chance to use it.
// | So, effectively... "no."
// |
// | Stylistically, a "modeless dialog" is implemented as a DzPane subclass in DAZ Studio.
// | And while portions of DzPane are exposed to script, you cannot construct one via script.
// | You must use the SDK for that.
//
// So we could provide a local Event Loop to process the App's events,
// while keeping the Dialog open until we really want it to close,
// and being careful to cater for the Non-Modal consequences.
// See items marked by: \\\NonModal
// Outstanding issues are marked by: ???Q
// Something like this...
// To minimize pollution of the script global namespace,
// use an anonymous function as this script's Main Routine:
// This limits the scope of this script's variables, etc.
// to just this script.
( function() {
//-------------------------------------------------------------
//---Script Globals---
//-------------------------------------------------------------
// See Note \\\NonModal_02:
// We need a flag to control our local Event Loop in executeTheScript(),
// and it must be accessible by the the code that Closes the Dialog:
var g_bTerminated = false;
//----------------------
//---Build the Dialog---
//----------------------
// Note \\\NonModal_04:
// --------------------
// If we create the Dialog with no Parent:
// var
// g_wDialog = new DzDialog;
// Then Qt will treat it as a "top-level" window,
// which is similar to being a separate Application,
// so User can close the DAZ Studio App and this Dialog will still survive,
// apparently running in its own execution thread in the Qt runtime system.
// (See Note \\\NonModal_05 for an attempt to prevent this).
// This is probably not the behaviour we want, so instead:
// Parent the Dialog in the App's MainWindow,
// so that the Dialog will be automatically Closed when the MainWindow closes:
// (A more accurate term for MainWindow here would be "Owner" rather than "Parent":
// MainWindow takes responsibility for destroying this Dialog.
// This Dialog's position is NOT constrained to be within MainWindow).
var
g_wDialog = new DzDialog( MainWindow );
g_wDialog.caption = "Non-Modal Dialog test";
g_wDialog.width = 400; // Make it a reasonable size, and wide enough to show g_wInfoText as 1 line
// Use a Layout so the controls auto-size nicely:
var
g_wDialogLayout = new DzVBoxLayout( g_wDialog );
g_wDialogLayout.autoAdd = true;
//---Property Controller---
// To be a realistic, useful test for a Non-Modal dialog, we need a bit of interaction with the Scene, so
// as an example add a Slider to control the selected Node's Y-Rotate property:
// (Not Scale, because e.g. Hip does not have that):
// Overall Property-Controller container:
var
g_wPropertyGroupBox = new DzVGroupBox( g_wDialog );
// Instructions to display when no Node is Selected:
var
g_wInfoText = new DzLabel( g_wPropertyGroupBox );
g_wInfoText.text = "Select a Node in the Scene... (Use the Node Selection Tool, or the Scene Pane)";
// Property-Value sub-container:
var
g_wValueGroupBox = new DzHGroupBox( g_wPropertyGroupBox );
// Slider to set the Value of the Property:
var
g_wValueSlider = new DzFloatSlider( g_wValueGroupBox );
g_wValueSlider.label = "Y-Rotate:"; // Sometimes labelled "Twist", etc. instead
g_wValueSlider.labelVisible = true; // Override the default
g_wValueSlider.textEditable = true; // Override the default: Let User edit the value directly
g_wValueSlider.toolTip = "Select a Node in the Scene, then use this slider to control the Y-Rotation of the Node";
g_wValueSlider.whatThis = g_wValueSlider.toolTip; // If we don't set any .whatsThis then the ? button captures the Mouse
connect( g_wValueSlider, "valueChanged(float)", g_wValueSlider_ProcessChange );
// Property Value Default Button: So User can easily reset to default value
var
g_wValueDefault = new DzPushButton( g_wValueGroupBox );
g_wValueDefault.text = "Default";
g_wValueDefault.toolTip = "Restore the property's default value";
g_wValueDefault.whatsThis = g_wValueDefault.toolTip; // If we don't set any .whatsThis then the ? button captures the Mouse
g_wValueDefault.autoDefault = false; // Override the default setting
g_wValueDefault.setFixedWidth( 50 );
connect( g_wValueDefault, "clicked()", restoreDefaultValue );
//---Close Button---
// Provide a Close button, so the User can choose when to close this Dialog:
var
g_wCloseButton = new DzPushButton( g_wDialog );
g_wCloseButton.text = "Close";
g_wCloseButton.toolTip = "Close the dialog";
g_wCloseButton.whatsThis = g_wCloseButton.toolTip; // If we don't set any .whatsThis then the ? button captures the Mouse
g_wCloseButton.setFixedHeight( 30 ); // Make it big and easy to click on
connect( g_wCloseButton, "clicked()", closeTheDialog );
//
// Note \\\NonModal_06:
// --------------------
// We don't want any Enter keypress to Close this Dialog,
// so we disable that default DzDialog behaviour
// by doing this for ALL DzPushButons we create in this Dialog:
g_wCloseButton.autoDefault = false; // Override the default setting
//---Connect the Dialog with the Scene---
// The Node in the Scene whose Property we are controlling:
var g_oNode = undefined; // : DzNode // Gets set by processSelectedNodeChange();
// We want to be notified whenever the Node Selection changes:
connect( Scene, "nodeSelectionListChanged()", processSelectedNodeChange );
// The numeric Property we are controlling:
var g_oFloatProperty = undefined; // : DzFloatProperty // Gets set by processSelectedNodeChange();
// Link the Node to the Dialog, and load the initial value:
processSelectedNodeChange();
//-------------------------------------------------------------
function closeTheDialog() // : void
{
// Event handler for g_wCloseButton.clicked()
// $$$ Testing: If the Dialog causes an exception, do both the Dialog and the Script (i.e. the Event Loop) get destroyed correctly?:
// throw( "Exception test." );
// Answer: Yes.
// See \\\NonModal_02:
// Signal our Event Loop in executeTheScript() that we want to Terminate this script:
terminateTheScript();
// Note \\\NonModal_03:
// --------------------
// We must Close the Dialog only AFTER calling terminateTheScript(),
// else we get this Script Error in our Event Loop in executeTheScript():
// | Error: cannot access member `g_bTerminated' of deleted QObject
//
// Close the Dialog:
// This is tidy, but not essential: The Script Environment would do this automatically
// once this script terminates, provided we built the Dialog per Note \\\NonModal_04 above:
g_wDialog.close();
}; // closeTheDialog()
//-------------------------------------------------------------
function g_wValueSlider_ProcessChange() // : void
{
// Event handler for g_wValueSlider.valueChanged()
// Load the Property's value from our control:
if( g_oNode ) {
if( g_oFloatProperty ) {
g_oFloatProperty.setValue( g_wValueSlider.value );
}
}
}; // g_wValueSlider_ProcessChange()
//-------------------------------------------------------------
function restoreDefaultValue() // : void
{
// Event handler for g_wValueDefault.clicked()
// Restore the Property's default value:
if( g_oNode ) {
if( g_oFloatProperty ) {
g_wValueSlider.value = g_oFloatProperty.getDefaultValue();
}
}
}; // restoreDefaultValue()
//-------------------------------------------------------------
function processSelectedNodeChange() // : void
{
// Event handler for Scene.SelectedNodeChanged()
// Disconnect any previously-current Node from the Dialog:
if( g_oNode ) {
disconnect( g_oNode, "transformChanged()", processNodePropertyValueChange );
}
// Save the updated reference:
g_oNode = Scene.getPrimarySelection(); // NB: Can = undefined
// Connect the current Node to the Dialog:
if( g_oNode ) {
// A Node is Selected:
g_wInfoText.hide();
g_wValueGroupBox.show();
// DzNode has no specific "rotationChanged()" signal, so we use the general signal:
connect( g_oNode, "transformChanged()", processNodePropertyValueChange );
// Load the Property's value and settings into our control:
g_oFloatProperty = g_oNode.getToolYRotControl();
if( g_oFloatProperty ) { // DAZ Script docs for DzNode say that this will always be true
g_wValueSlider.label = g_oNode.getLabel()+"."+g_oFloatProperty.getLabel();
g_wValueSlider.value = g_oFloatProperty.getValue();
g_wValueSlider.max = g_oFloatProperty.getMax();
g_wValueSlider.min = g_oFloatProperty.getMin();
g_wValueSlider.sensitivity = g_oFloatProperty.getSensitivity();
g_wValueSlider.displayAsPercent = g_oFloatProperty.getDisplayAsPercent();
}
} else {
// No Node is Selected:
g_wValueGroupBox.hide();
g_wInfoText.show();
}
}; // processSelectedNodeChange()
//-------------------------------------------------------------
function processNodePropertyValueChange() // : void
{
// Event handler for g_oNode.transformChanged()
// Load the Property's value into our control (if necessary):
if( g_oFloatProperty ) {
// Prevent recursion between this function
// processNodePropertyValueChange()
// and g_wValueSlider_ProcessChange():
// (This appears not to be necessary for most Properties, but play safe).
// NB: When comparing equality of fractional Numbers, treat equality to 6 decimals as "Equal":
// e.g.: eval( 0.1 + 0.2 == 0.3 ) returns false!
if( g_wValueSlider.value.toFixed(6) != g_oFloatProperty.getValue().toFixed(6) ) {
g_wValueSlider.value = g_oFloatProperty.getValue();
// Triggers a call to g_wValueSlider_ProcessChange()
}
}
}; // processNodePropertyValueChange()
//-------------------------------------------------------------
function terminateTheScript() // : void
{
// Signal the Event Loop in executeTheScript() to Terminate.
// See Note \\\NonModal_03:
// The Dialog must still be open here,
// else we get this Script Error in our Event Loop in executeTheScript():
// | Error: cannot access member `g_bTerminated' of deleted QObject
// See Note \\\NonModal_02:
// Signal our Event Loop in executeTheScript() that we want to Terminate this script:
g_bTerminated = true;
}; // terminateTheScript()
//-------------------------------------------------------------
function executeTheScript( p_Modal ) // : void
// p_Modal : Boolean // true==Modal, false==Non-Modal
{
// Launch the Dialog:
if( p_Modal ) {
//-----------
//---Modal---
//-----------
g_wDialog.caption = "Modal Dialog test";
g_wDialog.exec();
// Execution resumes here only after g_wDialog has Closed.
// // $$$ Testing: Can we still interact with the Scene, User Interface, etc. here?
// // i.e. The Dialog has Closed, but has not necessarily been Deleted yet.
// // Answer: Yes
// if( ( MessageBox.question( "Test after-Dialog operations?", "Modal Dialog", "Yes", "No", "" ) == 0 ) ) {
// // User pressed Yes
//
// // This works OK here:
// // i.e. The Dialog has been Closed, but not Deleted - just Hidden.
// g_wValueSlider.value = 90;
//
// MessageBox.information( "Press OK to continue...", "Modal Dialog post-processing Tested", "OK" );
//
// // $$$ Testing: Can we still re-launch it?:
// g_wDialog.caption = "Modal Dialog test #2";
// g_wDialog.exec();
// // Answer: Yes
// // If we wanted, we could probably relaunch it Non-Modally too.
//
// }
} else {
//---------------
//---Non-Modal---
//---------------
// Launch the Dialog and the local Event Loop:
//---Launch the Dialog Non-Modally---
g_wDialog.caption = "Non-Modal Dialog test";
// Note \\\NonModal_01:
// -------------------
// See info re .modal in "DAZStudio4 SDK/docs/qt/qtdialog.html"
// Instead of g_wDialog.exec(),
// which does not return until something Closes the Dialog,
// do this to launch Non-Modally:
g_wDialog.modal = false;
g_wDialog.show();
// Execution continues here immediately...
//---Local Event Loop---
// See Note \\\NonModal_05:
// If the Dialog becomes Hidden, we ASSUME is has Closed, and so we Auto-Terminate this loop.
// But only after it has first been Shown - so we need a flag to track that:
// DzDialog has no OnShow Event, so we must make our own equivalent:
var DialogShown = false;
// Extra safety precaution:
// Try to ensure this event loop gets terminated at the latest when the App Terminates:
connect( MainWindow, "aboutToClose()", terminateTheScript );
// Note \\\NonModal_02:
// --------------------
// We need a local loop that continuously calls processEvents():
// Without this, the Dialog flashes on screen, then disappears and the script terminates:
// With this, the Dialog remains on-screen and usable until its Close button is clicked,
// and all other Interface facilities are available as well - Camera controls, Parameters, etc.
g_bTerminated = false;
while( !g_bTerminated ) {
// See Note \\\NonModal_02:
// This is essential:
// Let the App process its Events, including our own g_wCloseButton.clicked() event,
// until our g_wCloseButton.clicked() event sets g_bTerminated = true:
// This allows the User to use all the standard interface controls that are not part of the Dialog,
// e.g. Camera controls, DrawStyle, Parameters, etc.
processEvents();
// NB: At this point, g_bTerminated may = true, so we must test it before doing other stuff here:
if( !g_bTerminated ) {
// $$$ Testing: If we auto-Terminate here, does g_wDialog get automatically Closed?:
// g_bTerminated = true;
// Answer: Yes
//---Detect whether the Dialog has been Closed---
// Note \\\NonModal_05:
// --------------------
// If the User Closes the Dialog via the g_wCloseButton button we provided,
// then it calls terminateTheScript() and all is OK: This event loop will terminate.
// But the Dialog can be Closed in a number of ways:
// 1) g_wDialog.close() was executed.
// 2) The User clicked the X button in the Dialog's Title bar.
// 3) The User right-clicked the Dialog's Title bar, and chose Close from the popup menu.
// 4) The User pressed Alt+F4, which is the shortcut for 2) above.
// 5) An unhandled exception occured in some part of the Dialog's code.
// We must cater for ALL ways, otherwise this event loop will continue to run,
// with no way for the User to terminate it other than terminating the entire App.
// Ideally, DzDialog would provide a "finished()" signal that we could connect to
// terminateTheScript():
// See DAZStudio4 SDK/docs/qt/qdialog.html
// | void QDialog::finished( int result ) [signal ]
// | This is emitted when the dialog's result code has been set.
// But as of v4.8 this is not available, so we must test something else...
//
// When the Dialog is Closed, it is immediately Hidden too - which is something which we CAN test:
// But we need to test DialogShown as well, else the Dialog would never appear,
// because the 1st pass thru this loop would set g_bTerminated = true:
if( g_wDialog.getWidget().visible ) {
// Signal that we can now Auto-Terminate the Dialog if it becomes hidden:
DialogShown = true;
} else {
// The Dialog is not visible
if( DialogShown ) {
// The Dialog was visible, and now is not.
// Something has Hidden the Dialog without calling terminateTheScript().
// The Dialog has been Hidden, so we ASSUME it has Closed:
// If we don't detect this condition here and Auto-Terminate the script, then
// the Script appears to have hung, and the only way to correct it is
// to terminate the entire App.
// $$$
// print( "Dialog has been hidden: Auto-Terminated within Event Loop." );
// Auto-Terminate:
g_bTerminated = true;
} // The Dialog has been Hidden, so we ASSUME Closed
} // The Dialog is not visible
} // Not yet Terminated
if( !g_bTerminated ) {
// ???Q_01: This seems to be a good thing to do here?:
// Prevent this loop from using too many CPU cycles: Give other threads more time to execute.
// Note: This just slows down this local loop - not the App's normal processing.
sleep( 50 ); // Pause this script for a bit, without blocking the App's event loop
// ???Q_02: Is this necessary here?:
// See DAZStudio4 SDK/docs/qt/qtcoreapplication.html#processEvents
// | In event you are running a local loop which calls processEvents() continuously,
// | without an event loop, the DefferredDelete events will not be processed.
// | This can affect the behaviour of widgets, e.g. QToolTip,
// | that rely on DeferredDelete events to function properly.
// | An alternative would be to call sendPostedEvents() from within that local loop.
//
// These both cause errors here:
// sendPostedEvents();
// App.sendPostedEvents();
//
// Is this equivalent? Or does it just slow the App down?
gc(); // Tell the global Garbage Collector to do its thing
} // Not yet Terminated
} // Until something sets g_bTerminated = true
} // Non-Modal
}; // executeTheScript()
//-------------------------------------------------------------
// Launch the Dialog, etc.:
// We could e.g. Launch Modally if the Ctrl key is down, etc.:
executeTheScript( false ); // true==Modal, false==Non-Modal
// Finalize the anonymous function, and immediately invoke it as this script's Main Routine:
} ) ();