User Tools

Site Tools


Save Filter Template Batch

Summary

Below is an example demonstrating how to read recorded settings for a save filter from a JSON file and use them to batch control saving from that save filter.

Note: This script is dependent on the Save Filter Template Apply sample residing in the same directory.

See Also:

API Areas of Interest

Example

SaveFilter_Template_Batch.dsa
// Define an anonymous function;
// serves as our main loop,
// limits the scope of variables
(function(){
 
	// Initialize 'static' variables that hold modifier key state
	var s_bShiftPressed = false;
	var s_bControlPressed = false;
	var s_bAltPressed = false;
	var s_bMetaPressed = false;
 
	// If the "Action" global transient is defined, and its the correct type
	if( typeof( Action ) != "undefined" && Action.inherits( "DzScriptAction" ) ){
		// If the current key sequence for the action is not pressed
		if( !App.isKeySequenceDown( Action.shortcut ) ){
			updateModifierKeyState();
		}
	// If the "Action" global transient is not defined
	} else if( typeof( Action ) == "undefined" ) {
		updateModifierKeyState();
	}
 
	/*********************************************************************/
	// void : A function for updating the keyboard modifier state
	function updateModifierKeyState()
	{
		// Get the current modifier key state
		var nModifierState = App.modifierKeyState();
		// Update variables that hold modifier key state
		s_bShiftPressed = (nModifierState & 0x02000000) != 0;
		s_bControlPressed = (nModifierState & 0x04000000) != 0;
		s_bAltPressed = (nModifierState & 0x08000000) != 0;
		s_bMetaPressed = (nModifierState & 0x10000000) != 0;
	};
 
	/*********************************************************************/
	// void : A function for printing only if debugging
	function debug()
	{
		// If we are not debugging
		if( !s_bAltPressed ){
			// We are done...
			return;
		}
 
		// Convert the arguments object into an array
		var aArguments = [].slice.call( arguments );
 
		// Print the array
		print( aArguments.join(" ") );
	};
 
	/*********************************************************************/
	// String : A function for retrieving a translation if one exists
	function text( sText )
	{
		// If the version of the application supports qsTr()
		if( typeof( qsTr ) != "undefined" ){
			// Return the translated (if any) text
			return qsTr( sText );
		}
 
		// Return the original text
		return sText;
	};
 
	/*********************************************************************/
	// Boolean : A function for testing whether or not a QObject instance
	// inherits one of a list of types
	function inheritsType( oObject, aTypeNames )
	{
		// If the object does not define the 'inherits' function
		if( !oObject || typeof( oObject.inherits ) != "function" ){
			// We are done... it is not a QObject
			return false;
		}
 
		// Iterate over the list of type names
		for( var i = 0, nTypes = aTypeNames.length; i < nTypes; i += 1 ){
			// If the object does not inherit the 'current' type
			if( !oObject.inherits( aTypeNames[i] ) ){
				// Next!!
				continue;
			}
 
			// Return the result
			return true;
		}
 
		// Return the result
		return false;
	};
 
	/*********************************************************************/
	// DzNode : A function for getting the root of a node
	function getRootNode( oNode )
	{
		// If we have a node and it is a bone
		if( oNode && inheritsType( oNode, ["DzBone"] ) ){
			// We want the skeleton
			return oNode.getSkeleton();
		}
 
		// Return the original node
		return oNode;
	};
 
	/*********************************************************************/
	// Object : A function for getting the value of a named member
	function getObjectValue( oObject, sMember, sType )
	{
		if( oObject.hasOwnProperty( sMember ) ){
			return oObject[ sMember ];
		}
 
		switch( sType ){
			case "object":
				return {};
			case "array":
				return [];
			case "number":
				return Number();
			case "string":
				return "";
			default:
				break;
		}
	};
 
	/*********************************************************************/
	// String : A function for getting the path of a script that resides next to this script
	function getAdjacentScriptPath( sName, bNotify )
	{
		// Create a file info object for this file
		var oFileInfo = new DzFileInfo( getScriptFileName() );
 
		// Define the base path of the script we will call; without the file extension
		var sBasePath = String( "%1/%2" ).arg( oFileInfo.path() ).arg( sName );
 
		// Clean up; do not leak memory
		oFileInfo.deleteLater();
 
		// Create a script object
		var oScript = new DzScript();
 
		// Attempt to find our script; doing it this way, we can debug with an
		// ascii file and distribute a binary [encrypted] file with the same name...
		// without having to update the contents of the script or manually handle
		// the file extensions; requires 3.0.1.5 or newer
		var sScriptPath = oScript.getScriptFile( sBasePath );
 
		// Clean up; do not leak memory
		oScript.deleteLater();
 
		// If a script is not found
		if( sScriptPath.isEmpty() && bNotify ){
			// Define message strings
			var sTitle = text( "File Not Found" );
			var sMessage = text( "A '%1.ds(a|b|e)' file could not be found." ).arg( sBasePath );
			var sButton = text( "&OK" );
			// Inform the user
			MessageBox.information( sMessage, sTitle, sButton );
		}
 
		// Return the result
		return sScriptPath;
	};
 
	/*********************************************************************/
	// Boolean : A function for executing a script that resides next to this script
	function executeAdjacentScript( sName, aArgs )
	{
		// Initialize
		var bResult = true;
 
		// Get the path of our script
		var sScriptPath = getAdjacentScriptPath( sName, true );
 
		// If a script is found
		if( !sScriptPath.isEmpty() ){							
			// Create a script object
			var oScript = new DzScript();
 
			// If the script loads
			if( oScript.loadFromFile( sScriptPath ) ){
				// Execute the script with our argument
				if( !oScript.execute( aArgs ) ){
					// Update our variable
					bResult = false;
				}
			// If the script does not load
			} else {
				// Define message strings
				var sTitle = text( "Read Error" );
				var sMessage = text( "The '%1' file could not be loaded." ).arg( sScriptPath );
				var sButton = text( "&OK" );
				// Inform the user
				MessageBox.information( sMessage, sTitle, sButton );
			}
 
			// Clean up; do not leak memory
			oScript.deleteLater();
		}
 
		// Return the result
		return bResult;
	};
 
	/***********************************************************************
	***** DsInterface Prototype *****
	***********************************************************************/
 
	// Create an interface object
	var s_oGui = new DsInterface();
 
	/*********************************************************************/
	function DsInterface()
	{
		this.oStyle = App.getStyle();
		this.bUseStyle = App.version > 0x03000000;
 
		this.nMargin = this.bUseStyle ? this.oStyle.pixelMetric( "DZ_GeneralMargin" ) : 5;
		this.nSpacing = this.nMargin;
		this.nBtnHeight = this.bUseStyle ? this.oStyle.pixelMetric( "DZ_ButtonHeight" ) : 20;
 
		this.wDlg;
		this.wSaveFilterCmb;
		this.wTemplateCmbEdit;
		this.wDefaultLEdit;
		this.wSourceLEdit;
		this.wSourceRecurseCbx;
		this.wNativeExtGrp;
		this.wPoserExtGrp;
		this.wTargetLEdit;
 
		this.sNodeName, this.sBasePath, this.sExt;
	};
 
	/***********************************************************************/
	DsInterface.superclass = Object;
 
	/*********************************************************************/
 
	/*********************************************************************/
	// void : A method for building a save filter combobox
	DsInterface.prototype.buildSaveFilterCombobox = function( aExclude )
	{
		// Get the asset IO manager
		var oAssetIOMgr = App.getAssetIOMgr();
 
		// Declare variables we'll be using as we iterate
		var oAssetIOFilter, oSettings;
 
		// Define the list of filters; in display order
		var aFilters = [
			"DzSceneAssetFilter",
			"DzSceneSubsetAssetFilter",
			"DzHierarchicalMaterialAssetFilter",
			"DzHierarchicalPoseAssetFilter",
			"DzWearablesAssetFilter",
			"DzCharacterAssetFilter",
			"DzPropertiesAssetFilter",
			"DzShapingAssetFilter",
			"DzPoseAssetFilter",
			"DzMaterialAssetFilter",
			"DzShaderAssetFilter",
			"DzCameraAssetFilter",
			"DzLightAssetFilter",
			"DzRenderSettingsAssetFilter",
			"DzSimulationSettingsAssetFilter",
			"DzDFormAssetFilter",
			"DzLayerAssetFilter",
			"DzPuppeteerAssetFilter",
			"---",
			"DzSceneSupportAssetFilter",
			"DzNodeSupportAssetFilter",
			"DzMorphSupportAssetFilter",
			"DzUVSupportAssetFilter",
			"DzShaderSupportAssetFilter",
		];
 
		// Declare working variables
		var sFilter;
		var nIdx;
 
		// Get the number of items in our list
		var nFilters = aFilters.length;
		// Iterate over our list 
		for( var i = 0; i < nFilters; i += 1 ){
			// Get the 'current' filter
			sFilter = aFilters[i];
			// If the 'filter' is in the exclude list
			if( aExclude.indexOf( sFilter ) > -1 )
			{
				// Next!!
				continue;
			}
 
			// If the 'filter' is a separator
			if( sFilter == "---" )
			{
				// Insert a separator at the end
				this.wSaveFilterCmb.insertSeparator( this.wSaveFilterCmb.count );
				// Next!!
				continue;
			}
 
			// Find the filter index
			nIdx = oAssetIOMgr.findFilter( sFilter );
 
			// If the filter was not found
			if( nIdx < 0 ){
				// Next!!
				continue;
			}
 
			// Get the filter
			oAssetIOFilter = oAssetIOMgr.getFilter( nIdx );
 
			// Add the filter to the choices
			this.wSaveFilterCmb.addItem(
					oAssetIOFilter.getDescription().replace( "...", "" ),
					oAssetIOFilter.className() );
 
			// Clean up; do not leak memory
			oAssetIOFilter.deleteLater();
		}
 
		// Insert a separator between filters we have
		// explicitly handled and those we have not
		this.wSaveFilterCmb.insertSeparator( this.wSaveFilterCmb.count );
 
		// Get the number of filters
		nFilters = oAssetIOMgr.getNumFilters();
		// Iterate over all filters
		for( var i = 0; i < nFilters; i += 1 ){
			// Get the 'current' filter
			oAssetIOFilter = oAssetIOMgr.getFilter( i );
 
			// Get the filter classname
			sFilter = oAssetIOFilter.className();
 
			// If the 'filter' has already been added
			// or it is in the exclude list
			if( aFilters.indexOf( sFilter ) > -1 ||
			aExclude.indexOf( sFilter ) > -1 ){
				// Clean up; do not leak memory
				oAssetIOFilter.deleteLater();
 
				// Next!!
				continue;
			}
 
			// Add the filter to the choices
			this.wSaveFilterCmb.addItem(
					oAssetIOFilter.getDescription().replace( "...", "" ),
					oAssetIOFilter.className() );
 
			// Clean up; do not leak memory
			oAssetIOFilter.deleteLater();
		}
	};
 
	/*********************************************************************/
	// void : A method for building a save filter template combobox
	DsInterface.prototype.buildTemplateCombobox = function()
	{
		// Construct the path for the template file
		this.sBasePath = String( "%1/Save Filter Settings Templates/%2/%3" )
							.arg( App.getAppDataPath() )
							.arg( this.getAssetFilterName() )
							.arg( this.sNodeName );
 
		// Define the file extension for the files
		this.sExt = "json";
 
		// Declare working variable
		var sPath;
 
		// Get the list of template files
		var aPaths = s_oFileSystem.getFileList(
						this.sBasePath, "*." + this.sExt, "String", true );
 
		// If the list is empty
		if( aPaths.length < 1 ){
			// Set the placeholder text
			this.wTemplateCmbEdit.placeholderText = text( "No templates found." );
 
			// We are done...
			return;
		}
 
		// Clear the placeholder text
		this.wTemplateCmbEdit.placeholderText = text( "Select a template..." );
 
		// Declare working variables
		var nStart, nEnd;
 
		// Iterate over the template paths
		for( var i = 0, nPaths = aPaths.length; i < nPaths; i += 1 ){
			// Get the 'current' path
			sPath = aPaths[ i ];
 
			// Set the start/end of the substring
			nStart = this.sBasePath.length + 1;
			nEnd = sPath.length - this.sExt.length - 1;
 
			// Get the relative basename
			sPath = sPath.substring( nStart, nEnd );
 
			// Add the item
			this.wTemplateCmbEdit.addItem( sPath );
		}
	};
 
	/*********************************************************************/
	// void : A method for handling when the save filter has changed
	DsInterface.prototype.handleSaveFilterChanged = function()
	{
		// Get the previous template
		var sPrevious = this.wTemplateCmbEdit.text;
 
		// Clear the template
		this.wTemplateCmbEdit.clear();
		this.wTemplateCmbEdit.text = "";
 
		// Rebuild the template options
		this.buildTemplateCombobox();
 
		// Attempt to find the previous item in the list
		var nIdx = this.wTemplateCmbEdit.findItem( sPrevious );
		// If the item is found
		if( nIdx > -1 ){
			// Restore the previous value
			this.wTemplateCmbEdit.text = sPrevious;
		}
 
		// Enable/disable based on valid options
		this.updateAcceptButtonEnabled();
	};
 
	/*********************************************************************/
	// void : A method for saving a template
	DsInterface.prototype.handleSaveTemplateClicked = function()
	{
		// Get the name of the asset filter
		var sAssetFilter = this.getAssetFilterName();
		// Define the list of filters that had a bug
		// with displaying their option dialogs
		var aExclude = [ "DzMaterialAssetFilter", "DzShaderAssetFilter" ];
 
		// If the version of the application is older than 4.11.0.229
		// and the current filter is one that had a bug prior to that
		if( App.version64 < 0x0004000b000000e5 &&
		aExclude.indexOf( sAssetFilter ) > -1 ){
			// Define message strings
			var sTitle = text( "Resource Error" );
			var sMessage = text( "Templates for the selected filter " +
				"cannot be created this way prior to version 4.11.0.229." );
			var sButton = text( "&OK" );
			// Inform the user
			MessageBox.information( sMessage, sTitle, sButton );
			// We are done...
			return;
		}
 
		// If the template save script executes successfully
		if( executeAdjacentScript(
			"SaveFilter_Template_Save",
			[ sAssetFilter ] ) ){
			// Cause the template selector to be rebuilt
			this.handleSaveFilterChanged();
		}
	};
 
	/*********************************************************************/
	// void : A method for browsing to a directory and updating a line edit
	DsInterface.prototype.handleBrowseDirClicked = function()
	{
		// Get the text from the line edit
		var sPath = this.text;
 
		// Prompt the user for a the path
		var sPath = FileDialog.doDirectoryDialog(
						text( "Choose a Directory" ), "", sPath );
 
		// If the user cancelled
		if( sPath.isEmpty() ){
			//We are done...
			return;
		}
 
		// Update the line edit
		this.text = sPath;
	};
 
	/*********************************************************************/
	// void : A method for browsing to a file and updating a line edit
	DsInterface.prototype.handleBrowseFileClicked = function()
	{
		// Get the text from the line edit
		var sPath = this.text;
 
		// Prompt the user for a the path
		var sPath = FileDialog.doFileDialog( true,
						text( "Choose a File" ), sPath,
						"DSON User File (*.duf)" );
 
		// If the user cancelled
		if( sPath.isEmpty() ){
			//We are done...
			return;
		}
 
		// Update the line edit
		this.text = sPath;
	};
 
	/*********************************************************************/
	// void : A method for comparing the source and target directories
	DsInterface.prototype.checkSourceTargetMatch = function()
	{
		// If the source is the same as the target
		if( this.wSourceLEdit.text == this.wTargetLEdit.text ){
			// Define common strings
			var sTitle = text( "Warning" );
			var sMessage = text( "The source and target are the same. " +
				"This will cause source files to be overwritten." );
			var sButton = text( "&OK" );
			// Inform the user
			MessageBox.warning( sMessage, sTitle, sButton, "" );
		}
	};
 
	/*********************************************************************/
	// String : A method for getting the current asset filter name
	DsInterface.prototype.getAssetFilterName = function()
	{
		// Return the classname of the selected asset filter
		return this.wSaveFilterCmb.itemData( this.wSaveFilterCmb.currentItem );
	};
 
	/*********************************************************************/
	// String : A method for getting the current template path
	DsInterface.prototype.getTemplatePath = function()
	{
		// Get the chosen template path
		var sRelPath = this.wTemplateCmbEdit.text;
		// If a template file has not been selected
		if( sRelPath.isEmpty() ){
			// Return an empty string
			return "";
		}
 
		// Return the re-constructed path
		return String( "%1/%2.%3" )
					.arg( this.sBasePath )
					.arg( sRelPath )
					.arg( this.sExt );
	};
 
	/*********************************************************************/
	// String : A method for getting the current default path
	DsInterface.prototype.getDefaultPath = function()
	{
		// Return the path
		return this.wDefaultLEdit.text;
	};
 
	/*********************************************************************/
	// String : A method for getting the current source path
	DsInterface.prototype.getSourcePath = function()
	{
		// Return the path
		return this.wSourceLEdit.text;
	};
 
	/*********************************************************************/
	// Array : A method for getting the file extensions
	DsInterface.prototype.getFileExtensions = function()
	{
		// Initialize
		var aExtensions = [];
 
		// Declare working variable
		var wBtn;
 
		// Get the list of buttons in the native group
		var aButtons = this.wNativeExtGrp.buttons();
		// Iterate over the buttons
		for( var i = 0, nButtons = aButtons.length; i < nButtons; i += 1 ){
			// Get the 'current' button
			wBtn = aButtons[ i ];
			// If the button is checked
			if( wBtn.checked ){
				// Add the extension to the list
				aExtensions.push( wBtn.text );
			}
		}
 
		// Get the list of buttons in the poser group
		aButtons = this.wPoserExtGrp.buttons();
		// Iterate over the buttons
		for( var i = 0, nButtons = aButtons.length; i < nButtons; i += 1 ){
			// Get the 'current' button
			wBtn = aButtons[ i ];
			// If the button is checked
			if( wBtn.checked ){
				// Add the extension to the list
				aExtensions.push( wBtn.text );
			}
		}
 
		// Return the path
		return aExtensions;
	};
 
	/*********************************************************************/
	// String : A method for getting the current target path
	DsInterface.prototype.getTargetPath = function()
	{
		// Return the path
		return this.wTargetLEdit.text;
	};
 
	/*********************************************************************/
	// void : A method for validating options
	DsInterface.prototype.getOptionsValid = function()
	{
		// Return whether or not all values are valid
		return !this.getAssetFilterName().isEmpty() &&
				!this.getTemplatePath().isEmpty() &&
				!this.getSourcePath().isEmpty() &&
				this.getFileExtensions().length > 0 &&
				!this.getTargetPath().isEmpty();
	};
 
	/*********************************************************************/
	// void : A method for updating the enabled state of the accept button
	DsInterface.prototype.updateAcceptButtonEnabled = function()
	{
		// Enable/disable based on valid options
		this.wDlg.setAcceptButtonEnabled( this.getOptionsValid() );
	};
 
	/*********************************************************************/
	// Object : A method for getting input from the user
	DsInterface.prototype.doDialog = function( sTitle, sNodeName, sSrcPath )
	{
		// Store the node name
		this.sNodeName = sNodeName;
 
		// Create the dialog
		this.wDlg = new DzBasicDialog();
 
		// Get the wrapped widget for the dialog
		var oDlgWgt = this.wDlg.getWidget();
 
		// Strip the space for a settings key
		var sKey = sTitle.replace( / /g, "" ) + "Dlg";
 
		// Set an [unique] object name on the wrapped dialog widget;
		// this is used for recording position and size separately
		// from all other [uniquely named] DzBasicDialog instances
		oDlgWgt.objectName = sKey;
 
		// Set the title of the dialog
		this.wDlg.caption = text( sTitle );
 
		// Create a widget
		var wMainWgt = new DzWidget( this.wDlg );
 
		// Create a grid layout
		var lytMain = new DzGridLayout( wMainWgt );
		lytMain.margin = this.nMargin;
		lytMain.spacing = this.nSpacing;
 
		// Initialize
		var nRow = 0;
 
		// Create a label
		var wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Filter :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a combo box
		this.wSaveFilterCmb = new DzComboBox( wMainWgt );
		// Set the height
		this.wSaveFilterCmb.setFixedHeight( this.nBtnHeight );
		// Connect
		this.wSaveFilterCmb["currentIndexChanged(int)"].connect( s_oGui, s_oGui.handleSaveFilterChanged );
		// Add the combo box to the layout
		lytMain.addWidget( this.wSaveFilterCmb, nRow, 1 );
 
		nRow += 1;
 
		// Create a label
		wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Template :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a widget
		var wTemplateWgt = new DzWidget( wMainWgt );
 
		// Create a horizontal box layout
		var lytTemplate = new DzHBoxLayout( wTemplateWgt );
		lytTemplate.margin = 0;
		lytTemplate.spacing = 0;
 
		// Create a combo edit
		this.wTemplateCmbEdit = new DzComboEdit( wTemplateWgt );
		this.wTemplateCmbEdit.submenuDelimiter = "/";
		this.wTemplateCmbEdit.menuSelectionIncludesPath = true;
		// Disable manual editing
		this.wTemplateCmbEdit.readOnly = true;
		// Connect
		this.wTemplateCmbEdit.textChanged.connect( s_oGui, s_oGui.updateAcceptButtonEnabled );
		// Add the combo edit to the layout
		lytTemplate.addWidget( this.wTemplateCmbEdit );
 
		// If the save script resides in the same folder as this one
		if( !getAdjacentScriptPath( "SaveFilter_Template_Save", false ).isEmpty() ){
			// Create a button
			var wSaveTemplateBtn = new DzPushButton( wTemplateWgt );
			wSaveTemplateBtn.text = "...";
			wSaveTemplateBtn.setFixedWidth( this.nBtnHeight );
			// Connect
			wSaveTemplateBtn.clicked.connect( s_oGui, s_oGui.handleSaveTemplateClicked );
			// Add the button to the layout
			lytTemplate.addWidget( wSaveTemplateBtn );
		}
 
		// Add the combo edit to the layout
		lytMain.addWidget( wTemplateWgt, nRow, 1 );
 
		nRow += 1;
 
		// Create a label
		wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Default :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a widget
		var wBrowseWgt = new DzWidget( wMainWgt );
 
		// Create a horizontal box layout
		var lytBrowse = new DzHBoxLayout( wBrowseWgt );
		lytBrowse.margin = 0;
		lytBrowse.spacing = 0;
 
		// Create a line edit
		this.wDefaultLEdit = new DzLineEdit( wBrowseWgt );
		this.wDefaultLEdit.placeholderText =
				text( "Select a file to apply between files..." );
		// Disable manual editing
		this.wDefaultLEdit.readOnly = true;
		// Add the line edit to the layout
		lytBrowse.addWidget( this.wDefaultLEdit );
 
		// Create a button
		var wBrowseBtn = new DzPushButton( wBrowseWgt );
		wBrowseBtn.text = "...";
		wBrowseBtn.setFixedWidth( this.nBtnHeight );
		// Connect
		wBrowseBtn.clicked.connect( this.wDefaultLEdit, s_oGui.handleBrowseFileClicked );
		// Add the button to the layout
		lytBrowse.addWidget( wBrowseBtn );
 
		// Add the browse wdget to the layout
		lytMain.addWidget( wBrowseWgt, nRow, 1 );
 
		nRow += 1;
 
		// Create a label
		wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Source :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a widget
		wBrowseWgt = new DzWidget( wMainWgt );
 
		// Create a horizontal box layout
		lytBrowse = new DzHBoxLayout( wBrowseWgt );
		lytBrowse.margin = 0;
		lytBrowse.spacing = 0;
 
		// Create a line edit
		this.wSourceLEdit = new DzLineEdit( wBrowseWgt );
		this.wSourceLEdit.text = sSrcPath;
		this.wSourceLEdit.placeholderText = text( "Select a source directory..." );
		// Disable manual editing
		this.wSourceLEdit.readOnly = true;
		// Connect
		this.wSourceLEdit.textChanged.connect( s_oGui, s_oGui.updateAcceptButtonEnabled );
		// Add the line edit to the layout
		lytBrowse.addWidget( this.wSourceLEdit );
 
		// Create a button
		wBrowseBtn = new DzPushButton( wBrowseWgt );
		wBrowseBtn.text = "...";
		wBrowseBtn.setFixedWidth( this.nBtnHeight );
		// Connect
		wBrowseBtn.clicked.connect( this.wSourceLEdit, s_oGui.handleBrowseDirClicked );
		// Add the button to the layout
		lytBrowse.addWidget( wBrowseBtn );
 
		// Add the browse wdget to the layout
		lytMain.addWidget( wBrowseWgt, nRow, 1 );
 
		nRow += 1;
 
		// Create a checkbox
		this.wSourceRecurseCbx = new DzCheckBox( wMainWgt );
		this.wSourceRecurseCbx.text = text( "Recursive" );
		lytMain.addWidget( this.wSourceRecurseCbx, nRow, 1 );
 
		nRow += 1;
 
		// Create a label
		wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Type(s) :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a button group box
		this.wNativeExtGrp = new DzHButtonGroup( wMainWgt );
		this.wNativeExtGrp.insideMargin = this.nMargin;
 
		// Define a list of native formats
		var aNativeFormats = [ "*.duf", "*.dsf", "*.ds", "*.dsa", "*.dsb", "*.dse" ];
		// Create a checkbox for each extension
		for( var i = 0, nFormats = aNativeFormats.length; i < nFormats; i += 1 ){
			// Create a checkbox
			wButton = new DzCheckBox( this.wNativeExtGrp );
			wButton.text = aNativeFormats[ i ];
			wButton.checked = i == 0;
			// Connect
			wButton.clicked.connect( s_oGui, s_oGui.updateAcceptButtonEnabled );
		}
 
		lytMain.addWidget( this.wNativeExtGrp, nRow, 1 );
 
		nRow += 1;
 
		// Create a button group box
		this.wPoserExtGrp = new DzHButtonGroup( wMainWgt );
		this.wPoserExtGrp.insideMargin = this.nMargin;
		this.wPoserExtGrp.columns = 2;
 
		// Define a list of poser formats
		var aPoserFormats = [ "*.pz2", "*.p2z", "*.fc2", "*.fcz", "*.hd2", "*.hdz",
							"*.lt2", "*.ltz", "*.cm2", "*.cmz", "*.mc6", "*.mcz" ];
		// Create a checkbox for each extension
		for( var i = 0, nFormats = aPoserFormats.length; i < nFormats; i += 1 ){
			// Create a checkbox
			wButton = new DzCheckBox( this.wPoserExtGrp );
			wButton.text = aPoserFormats[ i ];
			// Connect
			wButton.clicked.connect( s_oGui, s_oGui.updateAcceptButtonEnabled );
		}
 
		lytMain.addWidget( this.wPoserExtGrp, nRow, 1 );
 
		nRow += 1;
 
		// Create a label
		wLabel = new DzLabel( wMainWgt );
		wLabel.text = text( "Target :" );
		wLabel.alignment = DzWidget.AlignRight | DzWidget.AlignVCenter;
		// Add the label to the layout
		lytMain.addWidget( wLabel, nRow, 0 );
 
		// Create a widget
		wBrowseWgt = new DzWidget( wMainWgt );
 
		// Create a horizontal box layout
		lytBrowse = new DzHBoxLayout( wBrowseWgt );
		lytBrowse.margin = 0;
		lytBrowse.spacing = 0;
 
		// Create a line edit
		this.wTargetLEdit = new DzLineEdit( wBrowseWgt );
		this.wTargetLEdit.placeholderText = text( "Select a destination directory..." );
		// Disable manual editing
		this.wTargetLEdit.readOnly = true;
		// Connect
		this.wTargetLEdit.textChanged.connect( s_oGui, s_oGui.updateAcceptButtonEnabled );
		// Add the line edit to the layout
		lytBrowse.addWidget( this.wTargetLEdit );
 
		// Create a button
		wBrowseBtn = new DzPushButton( wBrowseWgt );
		wBrowseBtn.text = "...";
		wBrowseBtn.setFixedWidth( this.nBtnHeight );
		// Connect
		wBrowseBtn.clicked.connect( this.wTargetLEdit, s_oGui.handleBrowseDirClicked );
		// Add the button to the layout
		lytBrowse.addWidget( wBrowseBtn );
 
		// Add the browse wdget to the layout
		lytMain.addWidget( wBrowseWgt, nRow, 1 );
 
		// Connect
		this.wSourceLEdit.textChanged.connect( s_oGui, s_oGui.checkSourceTargetMatch );
		this.wTargetLEdit.textChanged.connect( s_oGui, s_oGui.checkSourceTargetMatch );
 
		nRow += 1;
 
		// Add the main widget to the dialog
		this.wDlg.addWidget( wMainWgt );
 
		// Get the wrapped widget for the dialog
		var oDlgWgt = this.wDlg.getWidget();
 
		// Get the minimum size of the dialog
		var sizeHint = oDlgWgt.minimumSizeHint;
 
		// Set the fixed height of the dialog
		this.wDlg.setFixedHeight( sizeHint.height );
		// Set the minimum width of the dialog
		this.wDlg.minWidth = 400;
 
		// Define the list of filters to exclude
		var aExclude = [ "DzSceneAssetFilter", "DzPuppeteerAssetFilter" ];
		// Populate the combobox with save filters
		this.buildSaveFilterCombobox( aExclude );
		// Populate the combobox with template 
		this.buildTemplateCombobox();
 
		// Enable/disable based on valid options
		this.updateAcceptButtonEnabled();
 
		// If the user accepts the dialog
		if( this.wDlg.exec() ){
			// Return the user's input
			return {
				"classname": this.getAssetFilterName(),
				"template": this.getTemplatePath(),
				"default": this.getDefaultPath(),
				"source": this.getSourcePath(),
				"recurse": this.wSourceRecurseCbx.checked,
				"types": this.getFileExtensions(),
				"target": this.getTargetPath()
			};
		// If the user rejects the dialog
		} else {
			// return an empty object
			return {};
		}
	};
 
	/***********************************************************************
	***** DsFileSystem Prototype *****
	***********************************************************************/
 
	// Create an interface object
	var s_oFileSystem = new DsFileSystem();
 
	/*********************************************************************/
	function DsFileSystem()
	{
	};
 
	/***********************************************************************/
	DsFileSystem.superclass = Object;
 
	/*********************************************************************/
	// Array<String|DzDir> : Method for collecting an array of directory objects
	DsFileSystem.prototype.getDirectories = function( oDir, regxFilter, nFilter, nSort, sType, bRecurse )
	{
		// Provide feedback
		debug( String("DsFileSystem::getDirectories( %1, \"%2\", %3, %4, \"%5\", %6 )")
			.arg( oDir.path() )
			.arg( regxFilter )
			.arg( nFilter )
			.arg( nSort )
			.arg( sType )
			.arg( bRecurse ) );
 
		// Declare working variable
		var sAbsPath;
 
		// Get the directory names
		var aDirs = oDir.entryList( regxFilter, nFilter, nSort );
		// Iterate over the directory names
		for( var i = 0, nDirs = aDirs.length; i < nDirs; i += 1 ){
			// Get the absolute path of the 'current' directory
			sAbsPath = String( "%1/%2" ).arg( oDir.absPath() ).arg( aDirs[ i ] );
			// Based on the type requested
			switch( sType ){
				default:
				case "String":
					// Update the name with the absolute path of the directory
					aDirs[ i ] = sAbsPath;
					// If we are recursing
					if( bRecurse ){
						// Recursively collect the directory paths
						aDirs = aDirs.concat( this.getSubDirectories( new DzDir( sAbsPath ),
							regxFilter, nFilter, nSort, sType ) );
					}
					break;
				case "DzDir":					
					// Update the name with a directory object
					aDirs[ i ] = new DzDir( sAbsPath );
					// If we are recursing
					if( bRecurse ){
						// Recursively collect the directories
						aDirs = aDirs.concat( this.getSubDirectories( aDirs[ i ],
							regxFilter, nFilter, nSort, sType ) );
					}
					break;
			}
		}
 
		// Return the result
		return aDirs;
	};
 
	/*********************************************************************/
	// Array<String|DzDir> : Method for recursively collecting an array of directory objects
	DsFileSystem.prototype.getSubDirectories = function( oDir, regxFilter, nFilter, nSort, sType )
	{
		// Provide feedback
		debug( String( "DsFileSystem::getSubDirectories( %1, \"%2\", %3, %4, \"%5\" )" )
			.arg( oDir.path() )
			.arg( regxFilter )
			.arg( nFilter )
			.arg( nSort )
			.arg( sType ) );
 
		// Initialize
		var aSubDirs = [];
 
		// Get the immediate child directories
		var aDirs = this.getDirectories( oDir, regxFilter, nFilter, nSort, sType, true );
		// Iterate over the directories
		for( var i = 0, nDirs = aDirs.length; i < nDirs; i += 1 ){
			// Based on the type requested
			switch( sType ){
				default:
				case "String":
					// Recursively collect the directory paths
					aSubDirs = aDirs.concat( this.getSubDirectories( new DzDir( aDirs[ i ] ),
						regxFilter, nFilter, nSort, sType ) );
					break;
				case "DzDir":	
					// Recursively collect the directories
					aSubDirs = aDirs.concat( this.getSubDirectories( aDirs[ i ],
						regxFilter, nFilter, nSort, sType, true ) );
					break;
			}
		}
 
		// Return the result
		return aSubDirs;
	};
 
	/*********************************************************************/
	// Array<String|DzFileInfo|DzFile> : Method for collecting an array of files
	DsFileSystem.prototype.getFiles = function( oDir, regxFilter, nFilter, nSort, sType )
	{
		// Provide feedback
		debug( String( "DsFileSystem::getFiles( %1, \"%2\", %3, %4, \"%5\" )" )
			.arg( oDir.path() )
			.arg( regxFilter )
			.arg( nFilter )
			.arg( nSort )
			.arg( sType ) );
 
		// Declare working variable
		var sAbsFilePath;
 
		// Get the file names
		var aFiles = oDir.entryList( regxFilter, nFilter, nSort );
		// Iterate over the file names
		for( var i = 0, nFiles = aFiles.length; i < nFiles; i += 1 ){
			// Get the absolute path of the 'current' file
			sAbsFilePath = oDir.absFilePath( aFiles[ i ] );
			// Based on the type requested
			switch( sType ){
				default:
				case "String":
					// Update the name with the absolute path of a file
					aFiles[ i ] = sAbsFilePath;
					break;
				case "DzFileInfo":
					// Update the name with a file info object
					aFiles[ i ] = new DzFileInfo( sAbsFilePath );
					break;
				case "DzFile":
					// Update the name with a file object
					aFiles[ i ] = new DzFile( sAbsFilePath );
					break;
			}
		}
 
		// Return the result
		return aFiles;
	};
 
	/*********************************************************************/
	// Array<String|DzDir> : Method for retrieving a list of directories
	DsFileSystem.prototype.getDirectoryList = function( sPath, sFilter, sType, bRecurse, bRelative )
	{
		// Declare the output
		var aDirs = [];
 
		// Create a directory object
		var oBaseDir = new DzDir( sPath );
		// If the directory doesn't exist
		if( !oBaseDir.exists() ){
			// Clean up; do not leak memory
			oBaseDir.deleteLater();
 
			// We are done...
			return aDirs;
		}
 
		// Get the directories
		var aDirs = this.getDirectories( oBaseDir, sFilter,
			DzDir.Dirs | DzDir.NoDotAndDotDot, DzDir.Name, sType, bRecurse );
 
		// Clean up; do not leak memory
		oBaseDir.deleteLater();
 
		// If we do not want relative paths
		if( !bRelative ){
			// Return the result
			return aDirs;
		}
 
		// Declare working variables
		var sAbsPath, sRelPath;
		var oDir;
 
		// Iterate over the directories
		for( var i = 0, nDirs = aDirs.length; i < nDirs; i += 1 ){
			// Based on the type requested
			switch( sType ){
				default:
				case "String":
					// Get the 'current' path
					sAbsPath = aDirs[ i ];
					// Get the relative portion of the path
					sRelPath = sAbsPath.substring( sPath.length );
					// Update the path
					aDirs[ i ] = sRelPath;
					break;
				case "DzDir":
					// Get the 'current' directory
					oDir = aDirs[ i ];
					// Get the path
					sAbsPath = oDir.path();
					// Get the relative portion of the path
					sRelPath = sAbsPath.substring( sPath.length );
					// Update the path
					oDir.setPath( sRelPath );
					// Update the directory
					aDirs[ i ] = oDir;
					break;
			}
		}
 
		// Return the result
		return aDirs;
	};
 
	/*********************************************************************/
	// Array<String|DzFileInfo|DzFile> : Method for retrieving a list of files
	DsFileSystem.prototype.getFileList = function( sPath, sFilter, sType, bRecurse )
	{
		// Initialize
		var aFiles = [];
 
		// Create a directory object
		var oBaseDir = new DzDir( sPath );
		// If the directory doesn't exist
		if( !oBaseDir.exists() ){
			// Clean up; do not leak memory
			oBaseDir.deleteLater();
 
			// We are done...
			return aFiles;
		}
 
		// Get the files from the specified directory
		aFiles = this.getFiles( oBaseDir, sFilter, DzDir.Files, DzDir.Name, sType );
 
		// Clean up; do not leak memory
		oBaseDir.deleteLater();
 
		// If we are recursing
		if( bRecurse ){	
			// Declare working variable
			var oDir;
 
			// Get the directories
			var aDirs = this.getDirectoryList( sPath, "*", "DzDir", bRecurse );
			// Iterate over the directories
			for( var i = 0, nDirs = aDirs.length; i < nDirs; i += 1 ){
				// Get the 'current' directory
				oDir = aDirs[ i ];
 
				// Append the files from the 'current' directory to the output
				aFiles = aFiles.concat(
					this.getFiles( oDir, sFilter, DzDir.Files, DzDir.Name, sType ) );
 
				// Clean up; do not leak memory
				oDir.deleteLater();
			}
		}
 
		// Return the result
		return aFiles;
	};
 
	/*********************************************************************/
	// Get the path of the script we will call; notify if it does not exist
	var sApplyScriptPath = getAdjacentScriptPath( "SaveFilter_Template_Apply", true );
 
	// If the script is not found
	if( sApplyScriptPath.isEmpty() ){
		// We are done...
		return;
	}
 
	// Declare working variables
	var sTitle, sMessage;
 
	// Define common strings
	var sButton = text( "&OK" );
 
	// Create a script object
	var oScript = new DzScript();
 
	// If the script does not load
	if( !oScript.loadFromFile( sApplyScriptPath ) ){
		// Define message strings
		sTitle = text( "Read Error" );
		sMessage = text( "The '%1' file could not be loaded." ).arg( sApplyScriptPath );
		// Inform the user
		MessageBox.information( sMessage, sTitle, sButton );
 
		// Clean up; do not leak memory
		oScript.deleteLater();
 
		// We are done..
		return;
	}
 
	// Get the root of the primary selection
	var oRootNode = getRootNode( Scene.getPrimarySelection() );
 
	// If we do not have a selection
	if( !oRootNode ){
		// Define message strings
		sTitle = text( "Selection Error" );
		sMessage = text( "A node in the scene must be selected to continue." );
		// Inform the user
		MessageBox.information( sMessage, sTitle, sButton );
 
		// Clean up; do not leak memory
		oScript.deleteLater();
 
		// We are done...
		return;
	}
 
	// Initialize
	var sPresetPath = "";
 
	// Get the pane manager
	var oPaneMgr = MainWindow.getPaneMgr();
	// If the pane manager was found
	if( oPaneMgr ){
		// Find the Content Library pane
		var oPane = oPaneMgr.findPane( "DzContentLibraryPane" );
		// If the pane was found
		if( oPane ){
			// Get the selected container
			var oContainer = oPane.getSelectedContainer();
			// If a native folder is selected
			if( inheritsType( oContainer, ["DzFolderAssetContainer"] ) /*&&
			oContainer.isNative*/ ){
				// Update the preset file path
				sPresetPath = oPane.getSelectedContainerPath();
			}
		}
	}
 
	// Prompt the user for input
	var oUserInput = s_oGui.doDialog(
				"Save Filter Template Batch",
				oRootNode.name,
				sPresetPath
			);
 
	// Get the classname of the filter to use
	var sClassName = getObjectValue( oUserInput, "classname", "string" );
	// If the user cancelled
	if( sClassName.isEmpty() ){
		// We are done...
		return;
	}
 
	// Get the path of the template file to use
	var sTemplateFile = getObjectValue( oUserInput, "template", "string" );
	// If the user cancelled
	if( sTemplateFile.isEmpty() ){
		// We are done...
		return;
	}
 
	// Get the default path
	var sDefaultPath = getObjectValue( oUserInput, "default", "string" );
 
	// Get the source path
	var sSourcePath = getObjectValue( oUserInput, "source", "string" );
	// If the user cancelled
	if( sSourcePath.isEmpty() ){
		// We are done...
		return;
	}
 
	// Get the source path
	var bSourceRecurse = getObjectValue( oUserInput, "recurse", "boolean" );
 
	// Get the source extensions
	var aExtensions = getObjectValue( oUserInput, "types", "array" );
	// If the user cancelled
	if( aExtensions.length < 1 ){
		// We are done...
		return;
	}
 
	// Get the preset path
	sPresetPath = getObjectValue( oUserInput, "target", "string" );
	// If the user cancelled
	if( sPresetPath.isEmpty() ){
		// We are done...
		return;
	}
 
	// Get the list of native files
	var aAssets = s_oFileSystem.getFileList( sSourcePath,
		aExtensions.join( " " ), "String", bSourceRecurse );
 
	// Get the number of assets
	var nAssets = aAssets.length;
 
	// Start the progress
	startProgress( text( "Batch Saving..." ), nAssets, true, true );
 
	// Create a file info object
	var oAssetFileInfo = new DzFileInfo( "" );
 
	//Declare working variable
	var sAsset, sAssetFilename, sPresetFile;
 
	// Get the content manager
	var oContentMgr = App.getContentMgr();
 
	// Iterate over the asset paths
	for( var i = 0; i < nAssets; i += 1 ){
		// Get the 'current' asset path
		sAsset = aAssets[i];
 
		// Update the file info object
		oAssetFileInfo.setFile( sAsset );
 
		// Load the file
		oContentMgr.openFile( sAsset );
 
		// Get the asset file name
		sAssetFilename = oAssetFileInfo.fileName();
 
		// Construct the preset file path
		sPresetFile = String( "%1/%2" )
						.arg( sPresetPath )
						.arg( sAssetFilename );
 
		// Post a status update
		App.statusLine( text( "Processing %1" ).arg( sAssetFilename ) );
 
		// Execute the script with our arguments
		oScript.execute( [ sClassName, sTemplateFile, sPresetFile ] );
 
		// Step the progress
		stepProgress();
 
		// If a default file was specified
		if( !sDefaultPath.isEmpty() ){
			// Load the file
			oContentMgr.openFile( sDefaultPath );
		}
 
		// Process events so that we can check user interaction
		processEvents();
 
		// If the user cancelled
		if( progressIsCancelled() ){
			// Post a status update
			App.statusLine( text( "User cancelled." ) );
			// We are done...
			break;
		}
	}
 
	// Finish the progress
	finishProgress();
 
	// Clean up; do not leak memory
	oScript.deleteLater();
	oAssetFileInfo.deleteLater();
 
// Finalize the function and invoke
})();