#target photoshop
//
// ContactSheetII
//
// Copyright: (c)2011. Adobe Systems, Incorporated.  All rights reserved.
// 
// Adobe legal gives me the privelege of removing x b y t o r @ g m a i l . c o m 
// as the author. Thanks for all the hard work x!!!
// 
//
//$.level = 1;
//

/*

@@@BUILDINFO@@@ ContactSheetII.jsx 2.2.15

*/

/*
// BEGIN__HARVEST_EXCEPTION_ZSTRING
<javascriptresource>
  <name>$$$/AdobePlugin/PIPLInfo/PluginName/ContactSheetII=Auto-Save-to-TIFF-From-Contact-Sheet-II...</name>
  <menu>automate</menu>
  <eventid>0B71D221-F8CE-11d2-B21B-0008C75B322Z</eventid>
<terminology><![CDATA[<< /Version 1
    /Events <<
       /0B71D221-F8CE-11d2-B21B-0008C75B322Z [($$$/AdobePlugin/PIPLInfo/PluginName/ContactSheetII=Auto-Save-to-TIFF-From-Contact-Sheet-II...) /contactSheetSettings <<
           /InSr [($$$/AETE/ContactSheet2/Source2=source) /typeInputSource]
           /InpD [($$$/AETE/ContactSheet2/Source=source) /typePlatformFilePath]
           /InSd [($$$/AETE/ContactSheet2/Directories=Directories) /typeBoolean]
           /Wdth [($$$/AETE/ContactSheet2/SheetWidth=sheet width) /typeUnitFloat]
           /Hght [($$$/AETE/ContactSheet2/SheetHeight=sheet height) /typeUnitFloat]
           /Unit [($$$/AETE/ContactSheet2/WidthHeightUnit=Width Height Unit) /typeWidthHeightUnit]
           /Rslt [($$$/AETE/ContactSheet2/Resolution=resolution) /typeUnitFloat]
           /RsUn [($$$/AETE/ContactSheet2/ResolutionUnit=Resolution Unit) /typeResolutionUnit]
           /Md   [($$$/AETE/ContactSheet2/Mode=mode) /typeColorSpace]
           /FltT [($$$/AETE/ContactSheet2/Flattened=Flattened Layers) /typeBoolean]
           /RowO [($$$/AETE/ContactSheet2/PlaceAcrossFirst=place across first) /typeBoolean]
           /Cols [($$$/AETE/ContactSheet2/NumberOfColumns=number of columns) /typeInteger]
           /Rows [($$$/AETE/ContactSheet2/NumberOfRows=number of rows) /typeInteger]
           /IndN [($$$/AETE/ContactSheet2/FilenameCaption=filename caption) /typeBoolean]
           /Font [($$$/AETE/ContactSheet2/Font=font) /typeFont]
           /FtSz [($$$/AETE/ContactSheet2/FontSize=font size) /typeInteger]
           /UAS  [($$$/AETE/ContactSheet2/UseAutoSpacing=Auto Spacing) /typeBoolean]
           /HtSp [($$$/AETE/ContactSheet2/HorizontalSpacing=Horizontal Spacing) /typeUnitFloat]
           /VtSp [($$$/AETE/ContactSheet2/VerticalSpacing=Vertical Spacing) /typeUnitFloat]
           /Rtbf [($$$/AETE/ContactSheet2/RotateForBestFit=Rotate For Best Fit) /typeBoolean]

           /CSIISettings [(ContactSheetII Settings) /typeText]
           /Msge [(Message) /typeText]
         >>]
     >>

    /Enums <<
        /typeFont <<
          /Sans ($$$/AETE/ContactSheet2/Mac/SansSerif=Helvetica)
          /Serf ($$$/AETE/ContactSheet2/Mac/Serif=Times)
          /Cour ($$$/AETE/ContactSheet2/Mac/Courier=Courier)
         >>
        /typeWidthHeightUnit <<
          /Inch ($$$/AETE/ContactSheet2/Inches=inches)
          /Ctmr ($$$/AETE/ContactSheet2/Centimeter=cm)
          /Pixl ($$$/AETE/ContactSheet2/Pixels=pixels)
         >>
        /typeResolutionUnit <<
          /PpIn ($$$/AETE/ContactSheet2/Pixels/Inches=pixels/inch)
          /PpCm ($$$/AETE/ContactSheet2/Pixels/Centimeter=pixels/cm)
         >>
        /typeInputSource <<
          /cuod ($$$/AETE/ContactSheet2/CurrentDocs=Open Documents)
          /fold ($$$/AETE/ContactSheet2/Folder=Folder)
          /flbr ($$$/AETE/ContactSheet2/Bridge=Bridge)
          /file ($$$/AETE/ContactSheet2/Files=Files)
         >>
     >>
   >>
   
]]></terminology>

</javascriptresource>
// END__HARVEST_EXCEPTION_ZSTRING
*/

/* fonts for Windows
        /typeFont <<
          /Sans ($$$/AETE/ContactSheet2/Win/SansSerif=Arial)
          /Serf ($$$/AETE/ContactSheet2/Win/Serif=Times New Roman)
          /Cour ($$$/AETE/ContactSheet2/Win/Courier=Courier)
         >>
*/

app;

$.localize = true;

//
//@show include
//
//
//
// psx.jsx
//   This file contains a collection code extracted from other parts
//   of xtools for use in production scripts written for Adobe.
//
// $Id: psx.jsx,v 1.63 2012/03/15 21:34:28 anonymous Exp $
//
//@show include
//
//

//
// cTID and sTID are wrappers for standard PS ID conversion functions.
// They require fewer keystrokes and are easier to read.
// Their implementations cache the calls to the underlying PS DOM
// functions making them fractionally faster than the underlying functions
// in some boundary cases.
//
cTID = function (s) { return cTID[s] || (cTID[s] = app.charIDToTypeID(s)); };
sTID = function (s) { return sTID[s] || (sTID[s] = app.stringIDToTypeID(s)); };

// return an ID for whatever s might be
xTID = function (s) {
    if (s.constructor == Number) {
        return s;
    }
    try {
        if (s instanceof XML) {
            var k = s.nodeKind();
            if (k == 'text' || k == 'attribute') {
                s = s.toString();
            }
        }
    } catch (e) {
    }

    if (s.constructor == String) {
        if (s.length > 0) {
            if (s.length != 4) return sTID(s);
            try { return cTID(s); } catch (e) { return sTID(s); }
        }
    }
    Error.runtimeError(19, s);  // Bad Argument

    return undefined;
};

//
// Convert a 32 bit ID back to either a 4 character representation or the
// mapped string representation.
//
id2char = function (s) {
    if (isNaN(Number(s))) {
        return '';
    }
    var v;

    var lvl = $.level;
    $.level = 0;
    try {
        if (!v) {
            try { v = app.typeIDToCharID(s); } catch (e) { }
        }
        if (!v) {
            try { v = app.typeIDToStringID(s); } catch (e) { }
        }
    } catch (e) {
    }
    $.level = lvl;

    if (!v) {
        // neither of the builtin PS functions know about this ID so we
        // force the matter
        v = psx.numberToAscii(s);
    }

    return v ? v : s;
};


//
// What platform are we on?
//
isWindows = function () { return $.os.match(/windows/i); };
isMac = function () { return !isWindows(); };

//
// Which app are we running in?
//
isPhotoshop = function () { return !!app.name.match(/photoshop/i); };
isBridge = function () { return !!app.name.match(/bridge/i); };

//
// Which CS version is this?
//
CSVersion = function () {
    var rev = Number(app.version.match(/^\d+/)[0]);
    return isPhotoshop() ? (rev - 7) : rev;
};
CSVersion._version = CSVersion();

isCS6 = function () { return CSVersion._version == 6; };
isCS5 = function () { return CSVersion._version == 5; };
isCS4 = function () { return CSVersion._version == 4; };
isCS3 = function () { return CSVersion._version == 3; };
isCS2 = function () { return CSVersion._version == 2; };
isCS = function () { return CSVersion._version == 1; };

//
// ZStrs is a container for (mostly) localized strings used in psx
// or elsewhere
//
try {
    var _lvl = $.level;
    $.level = 0;
    ZStrs;
} catch (e) {
    ZStrs = {};
} finally {
    $.level = _lvl;
    delete _lvl;
}

ZStrs.SelectFolder =
    localize("$$$/JavaScripts/psx/SelectFolder=Select a folder");

ZStrs.SelectFile =
    localize("$$$/JavaScripts/psx/SelectFile=Select a file");

ZStrs.FileErrorStr =
    localize("$$$/JavaScripts/psx/FileError=File Error: ");

ZStrs.BadFileSpecified =
    localize("$$$/JavaScripts/psx/BadFileSpecified=Bad file specified");

ZStrs.UnableToOpenLogFile =
    localize("$$$/JavaScripts/psx/UnableToOpenLogFile=Unable to open log file %%s : %%s");

ZStrs.UnableToWriteLogFile =
    localize("$$$/JavaScripts/psx/UnableToWriteLogFile=Unable to write to log file %%s : %%s");

ZStrs.UnableToOpenFile =
    localize("$$$/JavaScripts/psx/UnableToOpenFile=Unable to open file");

ZStrs.UnableToOpenInputFile =
    localize("$$$/JavaScripts/psx/UnableToOpenInputFile=Unable to open input file");

ZStrs.UnableToOpenOutputFile =
    localize("$$$/JavaScripts/psx/UnableToOpenOutputFile=Unable to open output file");

ZStrs.CharacterConversionError =
    localize("$$$/JavaScripts/psx/CharacterConversionError=Probable character conversion error");

// need to break up the ZString prefix to avoid
// the ZString harvester
ZStrs.InstalledScripts =
    localize('$' + '$' + '$' + '/' +
        (isCS6() ? "private/" : "") +
        "ScriptingSupport/InstalledScripts=Presets/Scripts");

ZStrs.DocumentName =
    localize("$$$/JavaScripts/psx/DocumentName=Document Name");

ZStrs.LCDocumentName =
    localize("$$$/JavaScripts/psx/LCDocumentName=document name");

ZStrs.UCDocumentName =
    localize("$$$/JavaScripts/psx/UCDocumentName=DOCUMENT NAME");

ZStrs.FN1Digit =
    localize("$$$/JavaScripts/psx/FN1Digit=1 Digit Serial Number");

ZStrs.FN2Digit =
    localize("$$$/JavaScripts/psx/FN2Digit=2 Digit Serial Number");

ZStrs.FN3Digit =
    localize("$$$/JavaScripts/psx/FN3Digit=3 Digit Serial Number");

ZStrs.FN4Digit =
    localize("$$$/JavaScripts/psx/FN4Digit=4 Digit Serial Number");

ZStrs.FN5Digit =
    localize("$$$/JavaScripts/psx/FN5Digit=5 Digit Serial Number");

ZStrs.LCSerial =
    localize("$$$/JavaScripts/psx/LCSerial=Serial Letter (a, b, c...)");

ZStrs.UCSerial =
    localize("$$$/JavaScripts/psx/UCSerial=Serial Letter (A, B, C...)");

ZStrs.Date_mmddyy =
    localize("$$$/JavaScripts/psx/Date/mmddyy=mmddyy (date)");

ZStrs.Date_mmdd =
    localize("$$$/JavaScripts/psx/Date/mmdd=mmdd (date)");

ZStrs.Date_yyyymmdd =
    localize("$$$/JavaScripts/psx/Date/yyyymmdd=yyyymmdd (date)");

ZStrs.Date_yymmdd =
    localize("$$$/JavaScripts/psx/Date/yymmdd=yymmdd (date)");

ZStrs.Date_yyddmm =
    localize("$$$/JavaScripts/psx/Date/yyddmm=yyddmm (date)");

ZStrs.Date_ddmmyy =
    localize("$$$/JavaScripts/psx/Date/ddmmyy=ddmmyy (date)");

ZStrs.Date_ddmm =
    localize("$$$/JavaScripts/psx/Date/ddmm=ddmm (date)");

ZStrs.Extension =
    localize("$$$/JavaScripts/psx/Extension=Extension");

ZStrs.LCExtension =
    localize("$$$/JavaScripts/psx/LCextension=extension");

ZStrs.UCExtension =
    localize("$$$/JavaScripts/psx/UCextension=EXTENSION");

ZStrs.FileNaming =
    localize("$$$/JavaScripts/psx/FileNaming=File Naming");

ZStrs.ExampleLabel =
    localize("$$$/JavaScripts/psx/ExampleLabel=Example:");

ZStrs.StartingSerialNumber =
    localize("$$$/JavaScripts/psx/StartingSerialNumber=Starting Serial #:");

ZStrs.CompatibilityPrompt =
    localize("$$$/JavaScripts/psx/CompatibilityPrompt=Compatibilty:");

ZStrs.Windows =
    localize("$$$/JavaScripts/psx/Windows=Windows");

ZStrs.MacOS =
    localize("$$$/JavaScripts/psx/MacOS=MacOS");

ZStrs.Unix =
    localize("$$$/JavaScripts/psx/Unix=Unix");

ZStrs.CustomTextEditor =
    localize("$$$/JavaScripts/psx/CustomTextEditor=Custom Text Editor");

ZStrs.CreateCustomText =
    localize("$$$/JavaScripts/psx/CreateCustomText=Create Custom Text");

ZStrs.EditCustomText =
    localize("$$$/JavaScripts/psx/EditCustomText=Edit Custom Text");

ZStrs.DeleteCustomText =
    localize("$$$/JavaScripts/psx/DeleteCustomText=Delete Custom Text");

ZStrs.DeleteCustomTextPrompt =
    localize("$$$/JavaScripts/psx/DeleteCustomTextPrompt=Do you really want to remove %%s?");

ZStrs.CustomTextPrompt =
    localize("$$$/JavaScripts/psx/CustomTextPrompt=Please enter the desired Custom Text: ");

ZStrs.Cancel =
    localize("$$$/JavaScripts/psx/Cancel=Cancel");

ZStrs.Save =
    localize("$$$/JavaScripts/psx/Save=Save");

ZStrs.UserCancelled =
    localize("$$$/ScriptingSupport/Error/UserCancelled=User cancelled the operation");

// Units
ZStrs.UnitsPX =
    localize("$$$/UnitSuffixes/Short/Px=px");

ZStrs.UnitsIN =
    localize("$$$/UnitSuffixes/Short/In=in");

ZStrs.Units_IN =
    localize("$$$/UnitSuffixes/Short/IN=in");

ZStrs.UnitsCM =
    localize("$$$/UnitSuffixes/Short/Cm=cm");

ZStrs.Units_CM =
    localize("$$$/UnitSuffixes/Short/CM=cm");

ZStrs.UnitsMM =
    localize("$$$/UnitSuffixes/Short/MM=mm");

ZStrs.UnitsPercent =
    localize("$$$/UnitSuffixes/Short/Percent=%");

ZStrs.UnitsPica =
    localize("$$$/UnitSuffixes/Short/Pica=pica");

ZStrs.UnitsPT =
    localize("$$$/UnitSuffixes/Short/Pt=pt");

ZStrs.UnitsShortCM =
    localize("$$$/UnitSuffixes/Short/CM=cm");

ZStrs.UnitsShortIn =
    localize("$$$/UnitSuffixes/Short/In=in");

ZStrs.UnitsShortIN =
    localize("$$$/UnitSuffixes/Short/IN=in");

ZStrs.UnitsShortMM =
    localize("$$$/UnitSuffixes/Short/MM=mm");

ZStrs.UnitsShortPercent =
    localize("$$$/UnitSuffixes/Short/Percent=%");

ZStrs.UnitsShortPica =
    localize("$$$/UnitSuffixes/Short/Pica=pica");

ZStrs.UnitsShortPT =
    localize("$$$/UnitSuffixes/Short/Pt=pt");

ZStrs.UnitsShortPx =
    localize("$$$/UnitSuffixes/Short/Px=px");

ZStrs.UnitsShortMMs =
    localize("$$$/UnitSuffixes/Short/MMs=mm");

ZStrs.UnitsShortPluralCMS =
    localize("$$$/UnitSuffixes/ShortPlural/CMS=cms");

ZStrs.UnitsShortPluralIns =
    localize("$$$/UnitSuffixes/ShortPlural/Ins=ins");

ZStrs.UnitsShortPluralPercent =
    localize("$$$/UnitSuffixes/ShortPlural/Percent=%");

ZStrs.UnitsShortPluralPicas =
    localize("$$$/UnitSuffixes/ShortPlural/Picas=picas");

ZStrs.UnitsShortPluralPts =
    localize("$$$/UnitSuffixes/ShortPlural/Pts=pts");

ZStrs.UnitsShortPluralPx =
    localize("$$$/UnitSuffixes/ShortPlural/Px=px");

ZStrs.UnitsVerboseCentimeter =
    localize("$$$/UnitSuffixes/Verbose/Centimeter=centimeter");

ZStrs.UnitsVerboseInch =
    localize("$$$/UnitSuffixes/Verbose/Inch=inch");

ZStrs.UnitsVerboseMillimeter =
    localize("$$$/UnitSuffixes/Verbose/Millimeter=millimeter");

ZStrs.UnitsVerbosePercent =
    localize("$$$/UnitSuffixes/Verbose/Percent=percent");

ZStrs.UnitsVerbosePica =
    localize("$$$/UnitSuffixes/Verbose/Pica=pica");

ZStrs.UnitsVerbosePixel =
    localize("$$$/UnitSuffixes/Verbose/Pixel=pixel");

ZStrs.UnitsVerbosePoint =
    localize("$$$/UnitSuffixes/Verbose/Point=point");

ZStrs.UnitsVerbosePluralCentimeters =
    localize("$$$/UnitSuffixes/VerbosePlural/Centimeters=Centimeters");

ZStrs.UnitsVerbosePluralInches =
    localize("$$$/UnitSuffixes/VerbosePlural/Inches=Inches");

ZStrs.UnitsVerbosePluralMillimeters =
    localize("$$$/UnitSuffixes/VerbosePlural/Millimeters=Millimeters");

ZStrs.UnitsVerbosePluralPercent =
    localize("$$$/UnitSuffixes/VerbosePlural/Percent=Percent");

ZStrs.UnitsVerbosePluralPicas =
    localize("$$$/UnitSuffixes/VerbosePlural/Picas=Picas");

ZStrs.UnitsVerbosePluralPixels =
    localize("$$$/UnitSuffixes/VerbosePlural/Pixels=Pixels");

ZStrs.UnitsVerbosePluralPoints =
    localize("$$$/UnitSuffixes/VerbosePlural/Points=Points");

ZStrs.FontLabel =
    localize("$$$/JavaScripts/psx/FontLabel=Font:");

ZStrs.FontTip =
    localize("$$$/JavaScripts/psx/FontTip=Select the font");

ZStrs.FontStyleTip =
    localize("$$$/JavaScripts/psx/FontStyleTip=Select the font style");

ZStrs.FontSizeTip =
    localize("$$$/JavaScripts/psx/FontSizeTip=Select the font size");


//
// Colors
//
ZStrs.black =
    localize("$$$/Actions/Enum/Black=black");
ZStrs.white =
    localize("$$$/Actions/Enum/White=white");
ZStrs.foreground =
    localize("$$$/JavaScripts/psx/Color/foreground=foreground");
ZStrs.background =
    localize("$$$/Actions/Enum/Background=background");
ZStrs.gray =
    localize("$$$/Actions/Enum/Gray=gray");
ZStrs.grey =
    localize("$$$/JavaScripts/psx/Color/grey=grey");
ZStrs.red =
    localize("$$$/Actions/Enum/Red=red");
ZStrs.green =
    localize("$$$/Actions/Enum/Green=green");
ZStrs.blue =
    localize("$$$/Actions/Enum/Blue=blue");

//
// Days of the week
//
ZStrs.Monday =
    localize("$$$/JavaScripts/psx/Date/Monday=Monday");
ZStrs.Mon =
    localize("$$$/JavaScripts/psx/Date/Mon=Mon");
ZStrs.Tuesday =
    localize("$$$/JavaScripts/psx/Date/Tuesday=Tuesday");
ZStrs.Tue =
    localize("$$$/JavaScripts/psx/Date/Tue=Tue");
ZStrs.Wednesday =
    localize("$$$/JavaScripts/psx/Date/Wednesday=Wednesday");
ZStrs.Wed =
    localize("$$$/JavaScripts/psx/Date/Wed=Wed");
ZStrs.Thursday =
    localize("$$$/JavaScripts/psx/Date/Thursday=Thursday");
ZStrs.Thu =
    localize("$$$/JavaScripts/psx/Date/Thu=Thu");
ZStrs.Friday =
    localize("$$$/JavaScripts/psx/Date/Friday=Friday");
ZStrs.Fri =
    localize("$$$/JavaScripts/psx/Date/Fri=Fri");
ZStrs.Saturday =
    localize("$$$/JavaScripts/psx/Date/Saturday=Saturday");
ZStrs.Sat =
    localize("$$$/JavaScripts/psx/Date/Sat=Sat");
ZStrs.Sunday =
    localize("$$$/JavaScripts/psx/Date/Sunday=Sunday");
ZStrs.Sun =
    localize("$$$/JavaScripts/psx/Date/Sun=Sun");


//
// Months
//
ZStrs.January =
    localize("$$$/JavaScripts/psx/Date/January=January");
ZStrs.Jan =
    localize("$$$/JavaScripts/psx/Date/Jan=Jan");
ZStrs.February =
    localize("$$$/JavaScripts/psx/Date/February=February");
ZStrs.Feb =
    localize("$$$/JavaScripts/psx/Date/Feb=Feb");
ZStrs.March =
    localize("$$$/JavaScripts/psx/Date/March=March");
ZStrs.Mar =
    localize("$$$/JavaScripts/psx/Date/Mar=Mar");
ZStrs.April =
    localize("$$$/JavaScripts/psx/Date/April=April");
ZStrs.Apr =
    localize("$$$/JavaScripts/psx/Date/Apr=Apr");
ZStrs.May =
    localize("$$$/JavaScripts/psx/Date/May=May");
ZStrs.June =
    localize("$$$/JavaScripts/psx/Date/June=June");
ZStrs.Jun =
    localize("$$$/JavaScripts/psx/Date/Jun=Jun");
ZStrs.July =
    localize("$$$/JavaScripts/psx/Date/July=July");
ZStrs.Jul =
    localize("$$$/JavaScripts/psx/Date/Jul=Jul");
ZStrs.August =
    localize("$$$/JavaScripts/psx/Date/August=August");
ZStrs.Aug =
    localize("$$$/JavaScripts/psx/Date/Aug=Aug");
ZStrs.September =
    localize("$$$/JavaScripts/psx/Date/September=September");
ZStrs.Sep =
    localize("$$$/JavaScripts/psx/Date/Sep=Sep");
ZStrs.October =
    localize("$$$/JavaScripts/psx/Date/October=October");
ZStrs.Oct =
    localize("$$$/JavaScripts/psx/Date/Oct=Oct");
ZStrs.November =
    localize("$$$/JavaScripts/psx/Date/November=November");
ZStrs.Nov =
    localize("$$$/JavaScripts/psx/Date/Nov=Nov");
ZStrs.December =
    localize("$$$/JavaScripts/psx/Date/December=December");
ZStrs.Dec =
    localize("$$$/JavaScripts/psx/Date/Dec=Dec");

ZStrs.AM =
    localize("$$$/JavaScripts/psx/Date/AM=AM");
ZStrs.PM =
    localize("$$$/JavaScripts/psx/Date/PM=PM");

//
// Color Profiles
//
ZStrs.ProfileAdobeRGB =
    localize("$$$/Menu/Primaries/AdobeRGB1998=Adobe RGB (1998)");

ZStrs.ProfileAppleRGB =
    localize("$$$/Actions/Enum/AppleRGB=Apple RGB");

ZStrs.ProfileProPhotoRGB =
    localize("$$$/JavaScripts/ContactSheet2/Profile/ProPhotoRGB=ProPhoto RGB");

ZStrs.ProfileSRGB =
    localize("$$$/JavaScripts/ContactSheet2/Profile/sRGB=sRGB IEC61966-2.1");

ZStrs.ProfileColorMatchRGB =
    localize("$$$/Actions/Enum/ColorMatch=ColorMatch RGB");

ZStrs.ProfileWideGamutRGB =
    localize("$$$/Actions/Enum/WideGamut=Wide Gamut RGB");

ZStrs.ProfileLab =
    localize("$$$/Actions/Enum/Lab=Lab");

// tpr not used
ZStrs.ProfileWorkingCMYK =
    localize("$$$/Actions/Key/ColorSettings/WorkingCMYK=Working CMYK");

// tpr not used
ZStrs.ProfileWorkingGray =
    localize("$$$/Actions/Key/ColorSettings/WorkingGray=Working Gray");

// tpr not used
ZStrs.ProfileWorkingRGB =
    localize("$$$/Actions/Key/ColorSettings/WorkingRGB=Working RGB");

//
// Color Modes
//
ZStrs.CMYKMode =
    localize("$$$/Menu/ModePopup/CMYKColor=CMYK Color");

ZStrs.GrayscaleMode =
    localize("$$$/Menu/ModePopup/Grayscale=Grayscale");

ZStrs.LabMode =
    localize("$$$/Menu/ModePopup/LabColor=Lab Color");

ZStrs.RGBMode =
    localize("$$$/Menu/ModePopup/RGBColor=RGB Color");

//
// psx works as a namespace for commonly used functions
//
psx = function () { };

// If IOEXCEPTIONS_ENABLED is true, psx File I/O operations
// perform strict error checking and throw IO_ERROR_CODE exceptions
// when errors are detected
psx.IOEXCEPTIONS_ENABLED = true;

// Generic psx error number
psx.ERROR_CODE = 9001;

// File IO error number used by psx functions
psx.IO_ERROR_CODE = 9002;

//
// Convert a 4 byte number back to a 4 character ASCII string.
//
psx.numberToAscii = function (n) {
    if (isNaN(n)) {
        return n;
    }
    var str = (String.fromCharCode(n >> 24) +
        String.fromCharCode((n >> 16) & 0xFF) +
        String.fromCharCode((n >> 8) & 0xFF) +
        String.fromCharCode(n & 0xFF));

    return (psx.isAscii(str[0]) && psx.isAscii(str[1]) &&
        psx.isAscii(str[2]) && psx.isAscii(str[3])) ? str : n;
};

//
// Character types...
//
psx.ASCII_SPECIAL = "\r\n !\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~";
psx.isSpecialChar = function (c) {
    return psx.ASCII_SPECIAL.contains(c[0]);
};
psx.isAscii = function (c) {
    return !!(c.match(/[\w\s]/) || psx.isSpecialChar(c));
};

//
// Define mappings between localized UnitValue type strings and strings
// acceptable to UnitValue constructors
//
psx._units = undefined;
psx._unitsInit = function () {
    if (!isPhotoshop()) {
        return;
    }
    psx._units = app.preferences.rulerUnits.toString();

    // map ruler units to localized strings
    psx._unitMap = {};
    psx._unitMap[Units.CM.toString()] = ZStrs.UnitsCM;
    psx._unitMap[Units.INCHES.toString()] = ZStrs.UnitsIN;
    psx._unitMap[Units.MM.toString()] = ZStrs.UnitsMM;
    psx._unitMap[Units.PERCENT.toString()] = ZStrs.UnitsPercent;
    psx._unitMap[Units.PICAS.toString()] = ZStrs.UnitsPica;
    psx._unitMap[Units.PIXELS.toString()] = ZStrs.UnitsPX;
    psx._unitMap[Units.POINTS.toString()] = ZStrs.UnitsPT;

    // since these are only used for construction UnitValue objects
    // don't bother with plural or verbose variants
    psx._unitStrMap = {};
    psx._reverseMap = {};

    function _addEntry(local, en) {
        psx._unitStrMap[local] = en;
        psx._unitStrMap[local.toLowerCase()] = en;
        psx._reverseMap[en.toLowerCase()] = local;
    }

    _addEntry(ZStrs.UnitsCM, "cm");
    _addEntry(ZStrs.UnitsShortCM, "cm");
    // _addEntry(ZStrs.UnitsShortPluralCMS, "cm");
    _addEntry(ZStrs.UnitsVerboseCentimeter, "centimeter");
    _addEntry(ZStrs.UnitsVerbosePluralCentimeters, "Centimeters");

    _addEntry(ZStrs.UnitsIN, "in");
    _addEntry(ZStrs.UnitsShortIN, "in");
    _addEntry(ZStrs.UnitsShortIn, "in");
    // _addEntry(ZStrs.UnitsShortPluralIns, "ins");
    _addEntry(ZStrs.UnitsVerboseInch, "inch");
    _addEntry(ZStrs.UnitsVerbosePluralInches, "Inches");

    _addEntry(ZStrs.UnitsMM, "mm");
    _addEntry(ZStrs.UnitsShortMM, "mm");
    // _addEntry(ZStrs.UnitsShortPluralMMs, "mm");
    _addEntry(ZStrs.UnitsVerboseMillimeter, "millimeter");
    _addEntry(ZStrs.UnitsVerbosePluralMillimeters, "Millimeters");

    _addEntry(ZStrs.UnitsPercent, "%");
    _addEntry(ZStrs.UnitsShortPercent, "%");
    _addEntry(ZStrs.UnitsShortPluralPercent, "%");
    _addEntry(ZStrs.UnitsVerbosePercent, "percent");
    _addEntry(ZStrs.UnitsVerbosePluralPercent, "Percent");

    _addEntry(ZStrs.UnitsPica, "pc");
    _addEntry(ZStrs.UnitsShortPica, "pc");
    _addEntry(ZStrs.UnitsShortPluralPicas, "picas");
    _addEntry(ZStrs.UnitsVerbosePica, "pica");
    _addEntry(ZStrs.UnitsVerbosePluralPicas, "Picas");

    _addEntry(ZStrs.UnitsPX, "px");
    _addEntry(ZStrs.UnitsShortPx, "px");
    _addEntry(ZStrs.UnitsShortPluralPx, "px");
    _addEntry(ZStrs.UnitsVerbosePixel, "pixel");
    _addEntry(ZStrs.UnitsVerbosePluralPixels, "Pixel");

    _addEntry(ZStrs.UnitsPT, "pt");
    _addEntry(ZStrs.UnitsShortPT, "pt");
    // _addEntry(ZStrs.UnitsShortPluralPts, "pt");
    _addEntry(ZStrs.UnitsVerbosePoint, "points");
    _addEntry(ZStrs.UnitsVerbosePluralPoints, "Points");
};
psx._unitsInit();


//
// Function: localizeUnitValue
// Description: Convert a UnitValue object to a localized string
// Input: un - UnitValue
// Return: a localized string
//
psx.localizeUnitValue = function (un) {
    var obj = {};
    obj.toString = function () {
        return this.value + ' ' + this.type;
    }
    obj.value = psx.localizeNumber(un.value);
    obj.type = un.type;

    var map = psx._unitStrMap;
    for (var idx in map) {
        if (un.type == map[idx]) {
            obj.type = idx;
            break;
        }
    }
    return obj;
};

//
// Function: localizeUnitType
// Description: Convert a UnitValue type string to a localized string
// Input: txt - UnitValue type string
// Return: a localized string
//
psx.localizeUnitType = function (txt) {
    var type = psx._reverseMap[txt.toLowerCase()];
    return type;
};

//
// Function: delocalizeUnitType
// Description: Convert a localized type to a UnitValue type string
// Input: txt - a localized type string
// Return: a UnitValue type string
//
psx.delocalizeUnitType = function (txt) {
    var type = psx._unitStrMap[txt.toLowerCase()];
    if (!type) {
        type = psx._unitStrMap[txt];
    }
    return type;
};


//
// Function: delocalizeUnitValue
// Description: Convert a localized UnitValue string into a UnitValue object
// Input: localized UnitValue string
// Return: a UnitValue object or undefined if there was a problem
//
psx.delocalizeUnitValue = function (str) {
    var un = undefined;
    var ar = str.split(/\s+/);
    if (ar.length == 2) {
        var n = psx.delocalizeNumber(ar[0]);
        var val = psx.delocalizeUnitType(ar[1]);
        un = UnitValue(n, val);
    }
    return un;
};

//
// Function: getDefaultUnits
// Description: gets the default ruler units as localized string
// Input: <input>
// Return: the default ruler units as localized string
//
psx.getDefaultUnits = function () {
    return psx._unitMap[psx._units];
};

//
// Function: getDefaultUnitsString
// Description: Get the ruler unit default Unit type
// Input: <none>
// Return: the default ruler unit as a UnitValue type
//
psx.getDefaultUnitsString = function () {
    return psx._unitStrMap[psx._unitMap[psx._units]];
};
psx.getDefaultRulerUnitsString = psx.getDefaultUnitsString;

//
// Function: validateUnitValue
// Description: Convert string to a UnitValue object
// Input: str - the string to be converted
//        bu  - the base UnitValue to use for conversion (opt)
//        ru  - the Unit type to use if one is not specified (opt)
//
// If bu is a Document, ru is set to the docs type and the
// docs resolution is used to determine the base UnitValue
//
// If ru is not specified, the default ruler unit type is used.
//
// If bu is not specified, a resolution of 1/72 is used.
//
// Note: this does not handle localized Unit value strings
//
// Return: A UnitValue object or undefined if it's not a valid string
//
psx.validateUnitValue = function (str, bu, ru) {
    var self = this;

    if (str instanceof UnitValue) {
        return str;
    }

    if (bu && bu instanceof Document) {
        var doc = bu;
        ru = doc.width.type;
        bu = UnitValue(1 / doc.resolution, ru);

    } else {
        if (!ru) {
            ru = psx.getDefaultRulerUnitsString();
        }
        if (!bu) {
            UnitValue.baseUnit = UnitValue(1 / 72, ru);
        }
    }
    str = str.toString().toLowerCase();

    var zero = new UnitValue("0 " + ru);
    var un = zero;
    if (!str.match(/[a-z%]+/)) {
        str += ' ' + ru.units;
    }
    str = psx.delocalizeNumber(s);
    un = new UnitValue(str);

    if (isNaN(un.value) || un.type == '?') {
        return undefined;
    }

    if (un.value == 0) {
        un = zero;
    }

    return un;
};


//
// Function: doEvent
// Description: Invoke a Photoshop Event with no arguments
// Input:  doc - the target document (opt: undefined)
//         eid - the event ID
//         interactive - do we run the event interactively (opt: true)
//         noDesc - do we pass in an empty descriptor (opt: true)
// Return: the result descriptor
//
psx.doEvent = function (doc, eid, interactive, noDesc) {
    var id;

    if (doc != undefined && eid == undefined) {
        if (doc.constructor == Number) {
            eid = doc.valueOf();
        } else if (doc.constructor == String) {
            eid = doc;
        }
        doc = undefined;
    }

    if (!eid) {
        Error.runtimeError(8600); // Event key is missing "No event id specified");
    }

    if (eid.constructor != Number) {
        if (eid.length < 4) {
            // "Event id must be at least 4 characters long"
            Error.runtimeError(19, "eventID");
        }

        if (eid.length == 4) {
            id = cTID(eid);
        } else {
            id = sTID(eid);
        }
    } else {
        id = eid;
    }

    interactive = (interactive == true);
    noDesc = (noDesc == true);

    function _ftn(id) {
        var dmode = (interactive ? DialogModes.ALL : DialogModes.NO);
        var desc = (noDesc ? undefined : new ActionDescriptor());
        return app.executeAction(id, desc, dmode);
    }

    return _ftn(id);
};


//
// Function: hist
// Description: Move back and forth through the history stack.
// Input: dir - "Prvs" or "Nxt "
// Return: <none>
//
psx.hist = function (dir) {
    function _ftn() {
        var desc = new ActionDescriptor();
        var ref = new ActionReference();
        ref.putEnumerated(cTID("HstS"), cTID("Ordn"), cTID(dir));
        desc.putReference(cTID("null"), ref);
        executeAction(cTID("slct"), desc, DialogModes.NO);
    }

    _ftn();
};

//
// Function: back
// Description: Move back through the history stack.
// Input: <none>
// Return: <none>
//
psx.undo = function () {
    psx.hist("Prvs");
};
//
// Function: redo
// Description: Move forward through the history stack.
// Input: <none>
// Return: <none>
//
psx.redo = function () {
    psx.hist("Nxt ");
};
//
// Function: Undo
// Description: Do an "Undo"
// Input: <none>
// Return: <none>
//
psx.Undo = function () {
    psx.doEvent("undo");
};
//
// Function: Redo
// Description: do a Redo
// Input: <none>
// Return: <none>
//
psx.Redo = function () {
    psx.doEvent(sTID('redo'));
};


//
// Function: delocalizeColorMode
// Description: Convert a localized mode string into a non-localized string.
//   This is useful for constructing API constants.
//   ex:
//     var mode = psx.delocalizeColorMode(ZStrs.LabMode);
//     doc.changeMode(eval("ChangeMode." + mode));
//   ex:
//     var mode = psx.delocalizeColorMode(ZStrs.LabMode);
//     var doc = Documents.add(UnitValue("6 in"), UnitValue("4 in"),
//                             300, "NoName", eval("NewDocumentMode." + mode));
// Input: a localized color mode string
// Return: a delocalized color mode string
//
psx.delocalizeColorMode = function (str) {
    var mode = str;

    switch (str) {
        case ZStrs.RGBMode: mode = "RGB"; break;
        case ZStrs.CMYKMode: mode = "CMYK"; break;
        case ZStrs.LabMode: mode = "Lab"; break;
        case ZStrs.GrayscaleMode: mode = "Grayscale"; break;
    }

    return mode;
};

//=========================== PS Paths =============================

//
// Some PS folder constants
//
// need to break up the ZString prefix to avoid
// the ZString harvester because the ZString definition changed
// in CS6
//
psx.PRESETS_FOLDER =
    new Folder(app.path + '/' +
        localize('$' + '$' + '$' + '/' +
            (isCS6() ? "private/" : "") +
            "ApplicationPresetsFolder/Presets=Presets"));

psx.SCRIPTS_FOLDER =
    new Folder(app.path + '/' + ZStrs.InstalledScripts);

psx.USER_PRESETS_FOLDER =
    new Folder(Folder.userData + '/' +
        localize("$$$/private/AdobeSystemFolder/Adobe=Adobe") + '/' +
        localize("$$$/private/FolderNames/AdobePhotoshopProductVersionFolder") + '/' +
        localize("$$$/private/FolderName/UserPresetsFolder/Presets=Presets"));


//======================= File functions ===========================

//
// Function: fileError
// Description: Format a standard File/Folder error string
// Input: f - File
//        msg - an error message (opt: '')
// Return: an File I/O error string
//
psx.fileError = function (f, msg) {
    return (ZStrs.FileErrorStr + (msg || '') + " \"" + decodeURI(f) +
        "\": " + f.error + '.');
};

//
// Function: convertFptr
// Description: convert something into a File/Folder object
// Input: fptr - a String, XML object, or existing File/Folder object
// Return: a File/Folder object
//
psx.convertFptr = function (fptr) {
    var f;
    try { if (fptr instanceof XML) fptr = fptr.toString(); } catch (e) { }

    if (fptr.constructor == String) {
        f = File(fptr);

    } else if (fptr instanceof File || fptr instanceof Folder) {
        f = fptr;

    } else {
        Error.runtimeError(19, "fptr");
    }
    return f;
};

//
// Function: writeToFile
// Description: Open a file, write a string into it, then close it
// Input: fptr - a file reference
//        str - a String
//        encoding - the encoding (opt)
//        lineFeed - the lineFeed (opt) 
// Return: <none>
//
psx.writeToFile = function (fptr, str, encoding, lineFeed) {
    var xfile = psx.convertFptr(fptr);
    var rc;

    if (encoding) {
        xfile.encoding = encoding;
    }

    rc = xfile.open("w");
    if (!rc) {
        Error.runtimeError(psx.IO_ERROR_CODE,
            psx.fileError(xfile, ZStrs.UnableToOpenOutputFile));
    }

    if (lineFeed) {
        xfile.lineFeed = lineFeed;
    }

    rc = xfile.write(str);
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, ZStrs.fileError(xfile));
    }

    rc = xfile.close();
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, ZStrs.fileError(xfile));
    }
};

//
// Function: readFromFile
// Description: Read the entire contents of a file as a string
// Input:  fptr - a file reference
//         encoding - the encoding (opt) 
//         lineFeed - the lineFeed (opt) 
// Returns: a String
// Note: there are some subtleties involved in handling
// some character conversions errors
//
psx.readFromFile = function (fptr, encoding, lineFeed) {
    var file = psx.convertFptr(fptr);
    var rc;

    rc = file.open("r");
    if (!rc) {
        Error.runtimeError(psx.IO_ERROR_CODE,
            psx.fileError(file, ZStrs.UnableToOpenInputFile));
    }
    if (encoding) {
        file.encoding = encoding;
    }
    if (lineFeed) {
        file.lineFeed = lineFeed;
    }
    var str = file.read();

    // In some situations, read() will set the file.error to
    // 'Character conversion error' but read the file anyway
    // In other situations it won't read anything at all from the file
    // we ignore the error if we were able to read the file anyway
    if (str.length == 0 && file.length != 0) {
        if (!file.error) {
            file.error = ZStrs.CharacterConversionError;
        }
        if (psx.IOEXCEPTIONS_ENABLED) {
            Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
        }
    } else {
        // if (file.error) {
        //   Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
        // }
    }

    rc = file.close();
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    return str;
};


//
// Function: readXMLFile
// Description: Reads a text file and returns an XML object.
//              psx assumes UTF8 with \n
// Input:  fptr - a reference to a file
// Return: an XML object
//
psx.readXMLFile = function (fptr) {
    var rc;
    var file = psx.convertFptr(fptr);
    if (!file.exists) {
        Error.runtimeError(48); // File/Folder does not exist
    }

    // Always work with UTF8/unix
    file.encoding = "UTF8";
    file.lineFeed = "unix";

    rc = file.open("r", "TEXT", "????");
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    var str = file.read();
    // Need additional error checking here...

    rc = file.close();
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    return new XML(str);
};

//
// Function: writeXMLFile
// Description: Writes an XML object to a file
//              psx uses UTF8 with \n
// Input:  fptr - a file reference
//         xml - an XML object
// Return: a File object
//
psx.writeXMLFile = function (fptr, xml) {
    var rc;
    if (!(xml instanceof XML)) {
        Error.runtimeError(19, "xml"); // "Bad XML parameter";
    }

    var file = psx.convertFptr(fptr);

    // Always work with UTF8/unix
    file.encoding = "UTF8";

    rc = file.open("w", "TEXT", "????");
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    // file.write("\uFEFF");
    // unicode signature, this is UTF16 but will convert to UTF8 "EF BB BF"
    // optional and not used since it confuses most programming editors
    // and command line tools

    file.lineFeed = "unix";

    file.writeln('<?xml version="1.0" encoding="utf-8"?>');

    rc = file.write(xml.toXMLString());
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    rc = file.close();
    if (!rc && psx.IOEXCEPTIONS_ENABLED) {
        Error.runtimeError(psx.IO_ERROR_CODE, psx.fileError(file));
    }

    return file;
};


//
// Function: psx.createFileSelect
// Description: File 'open' functions take a string of the format
//              "JPEG Files: *.jpg" on Windows and a function on
//              OS X. This function takes a Windows-style select string
//              a returns the OS X select-function on Mac.
//   ex:
//     var sel = psx.createFileSelect("XML Files: *.xml");
//     var file = psx.selectFileOpen(promptStr, sel, Folder.desktop);
// Input:  str - a Windows-style select string
// Return: The orignal select-string on Windows, or a select-function
//         for the select-string on OS X
//
psx.createFileSelect = function (str) {
    if (isWindows()) {
        return str;
    }

    if (!str.constructor == String) {
        return str;
    }

    var exts = [];
    var rex = /\*\.(\*|[\w]+)(.*)/;
    var m;
    while (m = rex.exec(str)) {
        exts.push(m[1].toLowerCase());
        str = m[2];
    }

    function macSelect(f) {
        var name = decodeURI(f.absoluteURI).toLowerCase();
        var _exts = macSelect.exts;

        // alert(name);

        while (f.alias) {
            try {
                f = f.resolve();
            } catch (e) {
                f = null;
            }

            if (f == null) {
                return false;
            }
        }

        if (f instanceof Folder) {
            return true;
        }

        for (var i = 0; i < _exts.length; i++) {
            var ext = _exts[i];
            if (ext == '.*') {
                return true;
            }
            if (name.match(RegExp("\\." + ext + "$", "i")) != null) {
                return true;
            }
        }
        return false;
    }

    macSelect.exts = exts;
    return macSelect;
};

//
// Function: selectFileOpen, selectFileSave
// Description: Open a dialog to prompt the user to select a file.
//              An initial file or folder can optionally be specified
//              Change the current directory reference if we it
//              seems appropriate.
//    ex: var file = psx.selectFileOpen("Choose a file to open",
//                                      "JPEG Files: *.jpg", "/c/tmp")
//    ex: var file = psx.selectFileSave("Choose a file to save",
//                                      "JPEG Files: *.jpg",
//                                      File("/c/tmp/tmp.jpg"))
// Input:  prompt - a prompt for the dialog (opt)
//         select - a select-string (opt)
//         start  - the initial directory
// Return: a File or undefined if the user canceled
//
psx.selectFileOpen = function (prompt, select, start) {
    return psx._selectFile(prompt, select, start, true);
};
psx.selectFileSave = function (prompt, select, start) {
    return psx._selectFile(prompt, select, start, false);
};
psx.selectFile = psx.selectFileOpen;

psx._selectFile = function (prompt, select, start, open) {
    var file;

    if (!prompt) {
        prompt = ZStrs.SelectFile;
    }

    if (start) {
        start = psx.convertFptr(start);
    } else {
        start = Folder.desktop;
    }

    var classFtn = (open ? File.openDialog : File.saveDialog);

    if (!start) {
        file = classFtn(prompt, select);

    } else {
        if (start instanceof Folder) {
            while (start && !start.exists) {
                start = start.parent;
            }

            var files = start.getFiles(select);
            if (!files || files.length == 0) {
                files = start.getFiles();
            }
            for (var i = 0; i < files.length; i++) {
                if (files[i] instanceof File) {
                    start = files[i];
                    break;
                }
            }
            if (start instanceof Folder) {
                start = new File(start + "/file");
            }
        }

        while (true) {
            if (start instanceof File) {
                var instanceFtn = (open ? "openDlg" : "saveDlg");
                file = start[instanceFtn](prompt, select);

            } else {
                file = Folder.selectDialog(prompt);
            }

            if (open && file && !file.exists) {
                continue;
            }

            break;
        }
    }

    if (file) {
        Folder.current = file.parent;
    }

    return file;
};

//
// Function: selectFolder
// Description: Open a dialog to select a folder
// Input:  prompt - (opt: "Select a Folder")
//         start - the initial folder
// Return: a Folder object or undefined if the user canceled
//
psx.selectFolder = function (prompt, start) {
    var folder;

    if (!prompt) {
        prompt = ZStrs.SelectFolder;
    }

    if (start) {
        start = psx.convertFptr(start);
        while (start && !start.exists) {
            start = start.parent;
        }
    }

    if (!start) {
        folder = Folder.selectDialog(prompt);

    } else {
        if (start instanceof File) {
            start = start.parent;
        }

        folder = start.selectDlg(prompt);
    }

    return folder;
};

//
// Function: getFiles
// Description: Get a set of files from a folder
// Input:  folder - a Folder
//         mask - a file mask pattern or RegExp (opt: undefined)
// Return: an array of Files
//
psx.getFiles = function (folder, mask) {
    var files = [];

    folder = psx.convertFptr(folder);

    if (folder.alias) {
        folder = folder.resolve();
    }

    return folder.getFiles(mask);
};

//
// Function: getFolders
// Description: Get a set of folders from a folder
// Input:  folder - a Folder
// Return: an array of Folders
//
psx.getFolders = function (folder) {
    folder = psx.convertFptr(folder);

    if (folder.alias) {
        folder = folder.resolve();
    }
    var folders = psx.getFiles(folder,
        function (f) { return f instanceof Folder; });
    return folders;
};

//
// Function: findFiles
// Description: Find a set of files from a folder recursively
// Input:  folder - a Folder
//         mask - a file mask pattern or RegExp (opt: undefined)
// Return: an array of Files
//
psx.findFiles = function (folder, mask) {
    folder = psx.convertFptr(folder);

    if (folder.alias) {
        folder = folder.resolve();
    }
    var files = psx.getFiles(folder, mask);
    var folders = psx.getFolders(folder);

    for (var i = 0; i < folders.length; i++) {
        var f = folders[i];
        var ffs = psx.findFiles(f, mask);
        // files.concat(ffs); This occasionally fails for some unknown reason (aka
        // interpreter Bug) so we do it manually instead
        while (ffs.length > 0) {
            files.push(ffs.shift());
        }
    }
    return files;
};

//
// Function: exceptionMessage
// Description: create a useful error message based on an exception
// Input: e - an Exception
// Return: a String
//
// Thanks to Bob Stucky for this...
//
psx.exceptionMessage = function (e) {
    var str = '';
    var fname = (!e.fileName ? '???' : decodeURI(e.fileName));
    str += "   Message: " + e.message + '\n';
    str += "   File: " + fname + '\n';
    str += "   Line: " + (e.line || '???') + '\n';
    str += "   Error Name: " + e.name + '\n';
    str += "   Error Number: " + e.number + '\n';

    if (e.source) {
        var srcArray = e.source.split("\n");
        var a = e.line - 10;
        var b = e.line + 10;
        var c = e.line - 1;
        if (a < 0) {
            a = 0;
        }
        if (b > srcArray.length) {
            b = srcArray.length;
        }
        for (var i = a; i < b; i++) {
            if (i == c) {
                str += "   Line: (" + (i + 1) + ") >> " + srcArray[i] + '\n';
            } else {
                str += "   Line: (" + (i + 1) + ")    " + srcArray[i] + '\n';
            }
        }
    }

    try {
        if ($.stack) {
            str += '\n' + $.stack + '\n';
        }
    } catch (e) {
    }

    if (str.length > psx.exceptionMessage._maxMsgLen) {
        str = str.substring(0, psx.exceptionMessage._maxMsgLen) + '...';
    }

    if (LogFile.defaultLog.fptr) {
        str += "\nLog File:" + LogFile.defaultLog.fptr.toUIString();
    }

    return str;
};
psx.exceptionMessage._maxMsgLen = 5000;

//============================ LogFile =================================

//
// Class: LogFile
// Description: provides a interface for logging information
// Input: fname - a file name
//
LogFile = function (fname) {
    var self = this;

    self.filename = fname;
    self.enabled = fname != undefined;
    self.encoding = "UTF8";
    self.append = false;
    self.fptr = undefined;
};

//
// Function: LogFile.setFilename
// Description: set the name of the log file. The log file is
//              enabled if a filename is passed in.
// Input: filename - the log filename or undefined
//        encoding - the file encoding (opt: "UTF8")
// Return: <none>
//
LogFile.prototype.setFilename = function (filename, encoding) {
    var self = this;
    self.filename = filename;
    self.enabled = filename != undefined;
    self.encoding = encoding || "UTF8";
    self.fptr = undefined;
};

//
// Function LogFile.write
// Description: Writes a string to a log file if the log is enabled
//              and it has a valid filename. The log file is opened
//              and closed for each write in order to flush the
//              message to disk.
// Input: msg - a message for the log file
// Return: <none>
//
LogFile.prototype.write = function (msg) {
    var self = this;
    var file;

    if (!self.enabled) {
        return;
    }

    if (!self.filename) {
        return;
    }

    if (!self.fptr) {
        file = new File(self.filename);
        if (self.append && file.exists) {
            if (!file.open("e", "TEXT", "????")) {
                var err = ZStrs.UnableToOpenLogFile.sprintf(file.toUIString(),
                    file.error);
                Error.runtimeError(psx.IO_ERROR_CODE, err);
            }
            file.seek(0, 2); // jump to the end of the file

        } else {
            if (!file.open("w", "TEXT", "????")) {
                if (!file.open("e", "TEXT", "????")) {
                    var err = ZStrs.UnableToOpenLogFile.sprintf(file.toUIString(),
                        file.error);
                    Error.runtimeError(psx.IO_ERROR_CODE, err);
                }
                file.seek(0, 0); // jump to the beginning of the file
            }
        }
        self.fptr = file;

    } else {
        file = self.fptr;
        if (!file.open("e", "TEXT", "????")) {
            var err = ZStrs.UnableToOpenLogFile.sprintf(file.toUIString(),
                file.error);
            Error.runtimeError(psx.IO_ERROR_CODE, err);
        }
        file.seek(0, 2); // jump to the end of the file
    }

    if (isMac()) {
        file.lineFeed = "Unix";
    }

    if (self.encoding) {
        file.encoding = self.encoding;
    }

    if (msg) {
        msg = msg.toString();
    }

    if (!file.writeln(new Date().toISODateString() + " - " + msg)) {
        var err = ZStrs.UnableToOpenLogFile.sprintf(file.toUIString(),
            file.error);
        Error.runtimeError(psx.IO_ERROR_CODE, err);
    }

    file.close();
};

//
// Function: LogFile.defaultLog 
// Description: This is the default log file
//
LogFile.defaultLog = new LogFile(Folder.userData + "/stdout.log");

//
// Function: LogFile.setFilename
// Description: sets the name of the default log file
// Input:  fptr - a file name
//         encoding - the encoding for the file (opt)
// Return: <none>
//
LogFile.setFilename = function (fptr, encoding) {
    LogFile.defaultLog.setFilename(fptr, encoding);
};

//
// Function: LogFile.write
// Description: write a message to the default log file
// Input:  msg - a message for the log file
// Return: <none>
//
LogFile.write = function (msg) {
    LogFile.defaultLog.write(msg);
};

//
// Function: LogFile.logException
// Description: log a formatted message based on an exception
// Input:  e - an Exception
//         msg - a message for the log file (opt)
//         doAlert - open an alert with the formatted message (opt: false)
// Return: <none>
//
LogFile.logException = function (e, msg, doAlert) {
    var log = LogFile.defaultLog;
    if (!log || !log.enabled) {
        return;
    }

    if (doAlert == undefined) {
        doAlert = false;

        if (msg == undefined) {
            msg = '';
        } else if (isBoolean(msg)) {
            doAlert = msg;
            msg = '';
        }
    }

    doAlert = !!doAlert;

    var str = ((msg || '') + "\n" +
        "==============Exception==============\n" +
        psx.exceptionMessage(e) +
        "\n==============End Exception==============\n");

    log.write(str);

    if (doAlert) {
        str += ("\r\r" + ZStrs.LogFileReferences + "\r" +
            "    " + log.fptr.toUIString());

        alert(str);
    }
};

//
// Function: toBoolean
// Description: convert something to a boolean
// Input:  s - the thing to convert
// Return: a boolean
//
function toBoolean(s) {
    if (s == undefined) { return false; }
    if (s.constructor == Boolean) { return s.valueOf(); }
    try { if (s instanceof XML) s = s.toString(); } catch (e) { }
    if (s.constructor == String) { return s.toLowerCase() == "true"; }

    return Boolean(s);
};

//
// Function: isBoolean
// Description: determine if something is a boolean
// Input:  s - the thing to test
// Return: true if s is boolean, false if not
//
function isBoolean(s) {
    return (s != undefined && s.constructor == Boolean);
};

//
// Description: Should the PS locale be used to determine the
//              decimal point or should the OS locale be used.
//              PS uses the OS locale so scripts may not match
//              the PS UI.
//
psx.USE_PS_LOCALE_FOR_DECIMAL_PT = true;

// 
// Function: determineDecimalPoint
// Description: determine what to use for the decimal point
// Input:  <none>
// Return: a locale-specific decimal point
//
// Note: Currently there is no way to determine what decimal
//       point is being used in the PS UI so this always returns
//       the decimal point for the PS locale
//
psx.determineDecimalPoint = function () {
    //   if (psx.USE_PS_LOCALE_FOR_DECIMAL_PT) {
    psx.decimalPoint = $.decimalPoint;
    //   }
    return psx.decimalPoint;
};
psx.determineDecimalPoint();

//
// Function: localizeNumber
// Description: convert a number to a string with a localized decimal point
// Input: n - a number or UnitValue
// Return: a number as a localized string
//
psx.localizeNumber = function (n) {
    return n.toString().replace('.', psx.decimalPoint);
};

//
// Function: delocalizeNumber
// Description: convert a string containing a localized number to
//              a "standard" number string
// Input:  a localized numeric string
// Return: a numeric string with a EN decimal point
//
psx.delocalizeNumber = function (n) {
    return n.toString().replace(psx.decimalPoint, '.');
};


//
// Function: toNumber
// Description: convert a something to a number
// Input: s - some representation of a number
//        def - a value to use if s cannot be parsed
// Return: a number or NaN if there was a problem and no default was specified
//
function toNumber(s, def) {
    if (s == undefined) { return def || NaN; }
    try { if (s instanceof XML) s = s.toString(); } catch (e) { }
    if (s.constructor == String && s.length == 0) { return def || NaN; }
    if (s.constructor == Number) { return s.valueOf(); }
    try {
        var n = Number(psx.delocalizeNumber(s.toString()));
    } catch (e) {
        // $.level = 1; debugger;
    }
    return (isNaN(n) ? (def || NaN) : n);
};

//
// Function: isNumber
// Description: see if something is a number
// Input: s - some representation of a number
//        def - a value to use if s cannot be parsed
// Return: true if s is a number, false if not
//
function isNumber(s) {
    try { if (s instanceof XML) s = s.toString(); }
    catch (e) { }
    return !isNaN(psx.delocalizeNumber(s));
};

//
// Function: isNumber
// Description: see if something is a String
// Input: s - something
// Return: true if s is a String, false if not
//
function isString(s) {
    return (s != undefined && s.constructor == String);
};

//
// Function: toFont
// Description: convert something to a font name
// Input: fs - a TextFont or a string
// Return: a font name that can be used with TextItem.font
//
function toFont(fs) {
    if (fs.typename == "TextFont") { return fs.postScriptName; }

    var str = fs.toString();
    var f = psx.determineFont(str);  // first, check by PS name

    return (f ? f.postScriptName : undefined);
};

// 
// Function: getXMLValue
// Description: returns the value of an xml object as a string if it
//              is not undefined else it returns a default value
// Input: xml - an XML object
//        def - a default value (opt: undefined)
// Return: a String or undefined
//
psx.getXMLValue = function (xml, def) {
    return (xml == undefined) ? def : xml.toString();
}

// 
// Function: getByName
// Description: Get an element in the container with a desired name property
// Input: container - an Array or something with a [] interface
//        value - the name of the element being sought
//        all - get all elements with the given name
// Return: an object, array of objects, or undefined
//
psx.getByName = function (container, value, all) {
    return psx.getByProperty(container, "name", value, all);
};

// 
// Function: getByProperty
// Description: Get an element in the container with a desired property
// Input: container - an Array or something with a [] interface
//        prop - the name of the property
//        value - the value of the property of the element being sought
//        all - get all elements that match
// Return: an object, array of objects, or undefined
//
psx.getByProperty = function (container, prop, value, all) {
    // check for a bad index
    if (prop == undefined) {
        Error.runtimeError(2, "prop");
    }
    if (value == undefined) {
        Error.runtimeError(2, "value");
    }
    var matchFtn;

    all = !!all;

    if (value instanceof RegExp) {
        matchFtn = function (s1, re) { return s1.match(re) != null; };
    } else {
        matchFtn = function (s1, s2) { return s1 == s2; };
    }

    var obj = [];

    for (var i = 0; i < container.length; i++) {
        if (matchFtn(container[i][prop], value)) {
            if (!all) {
                return container[i];     // there can be only one!
            }
            obj.push(container[i]);    // add it to the list
        }
    }

    return all ? obj : undefined;
};

//
// Function: determineFont
// Description: find a font based on a name
// Input: str - a font name or postScriptName
// Return: a TextFont or undefined
//
psx.determineFont = function (str) {
    return (psx.getByName(app.fonts, str) ||
        psx.getByProperty(app.fonts, 'postScriptName', str));
};

//
// Function: getDefaultFont
// Description: Attempt to find a resonable locale-specific font
// Input:  <none>
// Return: TextFont or undefined
//
psx.getDefaultFont = function () {
    var str;

    if (isMac()) {
        str = localize("$$$/Project/Effects/Icon/Font/Name/Mac=Lucida Grande");
    } else {
        str = localize("$$$/Project/Effects/Icon/Font/Name/Win=Tahoma");
    }

    var font = psx.determineFont(str);

    if (!font) {
        var f = psx.getApplicationProperty(sTID('fontLargeName'));
        if (f != undefined) {
            font = psx.determineFont(f);
        }
    }

    return font;
};

// 
// Function: psx.getDefaultTypeToolFont
// Description: This attemps gets the default Type Tool font. Since there is no
//         direct API for this, we have to save the current type tool settings,
//         reset the settings, then restore the saved settings.
//         This will fail if there already exists a tool preset called
//         "__temp__". Working around this shortcoming would make things even
//         more complex than they already are
// Input:  <none>
// Return: TextFont or undefined
//
psx.getDefaultTypeToolFont = function () {
    var str = undefined;
    var typeTool = "typeCreateOrEditTool";

    try {
        // get the current tool
        var ref = new ActionReference();
        ref.putEnumerated(cTID("capp"), cTID("Ordn"), cTID("Trgt"));
        var desc = executeActionGet(ref);
        var tid = desc.getEnumerationType(sTID('tool'));
        var currentTool = typeIDToStringID(tid);

        // switch to the type tool
        if (currentTool != typeTool) {
            var desc = new ActionDescriptor();
            var ref = new ActionReference();
            ref.putClass(sTID(typeTool));
            desc.putReference(cTID('null'), ref);
            executeAction(cTID('slct'), desc, DialogModes.NO);
        }

        var ref = new ActionReference();
        ref.putEnumerated(cTID("capp"), cTID("Ordn"), cTID("Trgt"));
        var desc = executeActionGet(ref);
        var tdesc = desc.hasKey(cTID('CrnT')) ?
            desc.getObjectValue(cTID('CrnT')) : undefined;

        if (tdesc) {
            // save the current type tool settings
            var desc4 = new ActionDescriptor();
            var ref4 = new ActionReference();
            ref4.putClass(sTID('toolPreset'));
            desc4.putReference(cTID('null'), ref4);
            var ref5 = new ActionReference();
            ref5.putProperty(cTID('Prpr'), cTID('CrnT'));
            ref5.putEnumerated(cTID('capp'), cTID('Ordn'), cTID('Trgt'));
            desc4.putReference(cTID('Usng'), ref5);
            desc4.putString(cTID('Nm  '), "__temp__");

            // this will fail if there is already a preset called __temp__
            executeAction(cTID('Mk  '), desc4, DialogModes.NO);

            // reset the type tool
            var desc2 = new ActionDescriptor();
            var ref2 = new ActionReference();
            ref2.putProperty(cTID('Prpr'), cTID('CrnT'));
            ref2.putEnumerated(cTID('capp'), cTID('Ordn'), cTID('Trgt'));
            desc2.putReference(cTID('null'), ref2);
            executeAction(cTID('Rset'), desc2, DialogModes.NO);

            // get the current type tool settings
            var ref = new ActionReference();
            ref.putEnumerated(cTID("capp"), cTID("Ordn"), cTID("Trgt"));
            var desc = executeActionGet(ref);
            var tdesc = desc.getObjectValue(cTID('CrnT'));

            // get the default type tool font
            var charOpts = tdesc.getObjectValue(sTID("textToolCharacterOptions"));
            var styleOpts = charOpts.getObjectValue(cTID("TxtS"));
            str = styleOpts.getString(sTID("fontPostScriptName"));

            // restore the type tool settings
            var desc9 = new ActionDescriptor();
            var ref10 = new ActionReference();
            ref10.putName(sTID('toolPreset'), "__temp__");
            desc9.putReference(cTID('null'), ref10);
            executeAction(cTID('slct'), desc9, DialogModes.NO);

            // delete the temp setting
            var desc11 = new ActionDescriptor();
            var ref12 = new ActionReference();
            ref12.putEnumerated(sTID('toolPreset'), cTID('Ordn'), cTID('Trgt'));
            desc11.putReference(cTID('null'), ref12);
            executeAction(cTID('Dlt '), desc11, DialogModes.NO);
        }

        // switch back to the original tool
        if (currentTool != typeTool) {
            var desc = new ActionDescriptor();
            var ref = new ActionReference();
            ref.putClass(tid);
            desc.putReference(cTID('null'), ref);
            executeAction(cTID('slct'), desc, DialogModes.NO);
        }
    } catch (e) {
        return undefined;
    }

    return str;
};

//
// Function: trim
// Description: Trim leading and trailing whitepace from a string
// Input: value - String
// Return: String
//
psx.trim = function (value) {
    return value.replace(/^[\s]+|[\s]+$/g, '');
};


//
// Function: copyFromTo
// Description: copy the properties from one object to another. functions
//              and 'typename' are skipped
// Input: from - object
//        to - Object
// Return: <none>
//
psx.copyFromTo = function (from, to) {
    if (!from || !to) {
        return;
    }
    for (var idx in from) {
        var v = from[idx];
        if (typeof v == 'function') {
            continue;
        }
        if (v == 'typename') {
            continue;
        }

        to[idx] = v;
    }
};

//
// Function: listProps
// Description: create a string with name-value pairs for each property
//              in an object. Functions are skipped.
// Input: obj - object
// Return: String
//
psx.listProps = function (obj) {
    var s = [];
    var sep = (isBridge() ? "\r" : "\r\n");

    for (var x in obj) {
        var str = x + ":\t";
        try {
            var o = obj[x];
            str += (typeof o == "function") ? "[function]" : o;
        } catch (e) {
        }
        s.push(str);
    }
    s.sort();

    return s.join(sep);
};


//
//============================ Strings  Extensions ===========================
//

//
// Function: String.contains
// Description: Determines if a string contains a substring
// Input: sub - a string
// Return: true if sub is a part of a string, false if not
//
String.prototype.contains = function (sub) {
    return this.indexOf(sub) != -1;
};

//
// Function: String.containsWord
// Description: Determines if a string contains a word
// Input: str - a word
// Return: true if str is word in a string, false if not
//
String.prototype.containsWord = function (str) {
    return this.match(new RegExp("\\b" + str + "\\b")) != null;
};

//
// Function: String.endsWith
// Description: Determines if a string ends with a substring
// Input: sub - a string
// Return: true if a string ends with sub, false if not
//
String.prototype.endsWith = function (sub) {
    return this.length >= sub.length &&
        this.slice(this.length - sub.length) == sub;
};

//
// Function: String.reverse
// Description: Creates a string with characters in reverse order
// Input:  <none>
// Return: the string with the characters in reverse order
//
String.prototype.reverse = function () {
    var ar = this.split('');
    ar.reverse();
    return ar.join('');
};

//
// Function: String.startsWith
// Description: Determines if a string starts with a substring
// Input: sub - a string
// Return: true if a string starts with sub, false if not
//
String.prototype.startsWith = function (sub) {
    return this.indexOf(sub) == 0;
};

//
// Function: String.trim
// Description: Trims whitespace from the beginning and end of a string
// Input:  <none>
// Return: the string with whitespace trimmed off
//
String.prototype.trim = function () {
    return this.replace(/^[\s]+|[\s]+$/g, '');
};
//
// Function: String.ltrim
// Description: Trims whitespace off the beginning of the string
// Input:  <none>
// Return: the string with whitespace trimmed off the start of the string
//
String.prototype.ltrim = function () {
    return this.replace(/^[\s]+/g, '');
};
//
// Function: String.rtrim
// Description: Trims whitespace off the end of a string
// Input:  <none>
// Return: the string with whitespace of the end of the string
//
String.prototype.rtrim = function () {
    return this.replace(/[\s]+$/g, '');
};


//========================= String formatting ================================
//
// Function: String.sprintf
// Description: Creates a formatted string
// Input: the format specification and values to be used in formatting
// Return: a formatted string
//
// Documentation:
//   http://www.opengroup.org/onlinepubs/007908799/xsh/fprintf.html
//
// From these sites:
//   http://forums.devshed.com/html-programming-1/sprintf-39065.html
//   http://jan.moesen.nu/code/javascript/sprintf-and-printf-in-javascript/
//
// Example:    var idx = 1;
//             while (file.exists) {
//                var newFname = "%s/%s_%02d.%s".sprintf(dir, fname,
//                                                       idx++, ext);
//                file = File(newFname);
//              }
//
String.prototype.sprintf = function () {
    var args = [this];
    for (var i = 0; i < arguments.length; i++) {
        args.push(arguments[i]);
    }
    return String.sprintf.apply(null, args);
};
String.sprintf = function () {
    function _sprintf() {
        if (!arguments || arguments.length < 1 || !RegExp) {
            return "Error";
        }
        var str = arguments[0];
        var re = /([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X)/m;
        //') /* for xemacs auto-indent  */
        var a = b = [], numSubstitutions = 0, numMatches = 0;
        var result = '';

        while (a = re.exec(str)) {
            var leftpart = a[1], pPad = a[2], pJustify = a[3], pMinLength = a[4];
            var pPrecision = a[5], pType = a[6], rightPart = a[7];

            rightPart = str.slice(a[0].length);

            numMatches++;

            if (pType == '%') {
                subst = '%';
            } else {
                numSubstitutions++;
                if (numSubstitutions >= arguments.length) {
                    alert('Error! Not enough function arguments (' +
                        (arguments.length - 1)
                        + ', excluding the string)\n'
                        + 'for the number of substitution parameters in string ('
                        + numSubstitutions + ' so far).');
                }
                var param = arguments[numSubstitutions];
                var pad = '';
                if (pPad && pPad.slice(0, 1) == "'") {
                    pad = leftpart.slice(1, 2);
                } else if (pPad) {
                    pad = pPad;
                }
                var justifyRight = true;
                if (pJustify && pJustify === "-") {
                    justifyRight = false;
                }
                var minLength = -1;
                if (pMinLength) {
                    minLength = toNumber(pMinLength);
                }
                var precision = -1;
                if (pPrecision && pType == 'f') {
                    precision = toNumber(pPrecision.substring(1));
                }
                var subst = param;
                switch (pType) {
                    case 'b':
                        subst = toNumber(param).toString(2);
                        break;
                    case 'c':
                        subst = String.fromCharCode(toNumber(param));
                        break;
                    case 'd':
                        subst = toNumber(param) ? Math.round(toNumber(param)) : 0;
                        break;
                    case 'u':
                        subst = Math.abs(Math.round(toNumber(param)));
                        break;
                    case 'f':
                        if (precision == -1) {
                            precision = 6;
                        }
                        var n = Number(parseFloat(param).toFixed(Math.min(precision, 20)));
                        subst = psx.localizeNumber(n);
                        //             ? Math.round(parseFloat(param) * Math.pow(10, precision))
                        //             / Math.pow(10, precision)
                        //             : ;
                        break;
                    case 'o':
                        subst = toNumber(param).toString(8);
                        break;
                    case 's':
                        subst = param;
                        break;
                    case 'x':
                        subst = ('' + toNumber(param).toString(16)).toLowerCase();
                        break;
                    case 'X':
                        subst = ('' + toNumber(param).toString(16)).toUpperCase();
                        break;
                }
                var padLeft = minLength - subst.toString().length;
                if (padLeft > 0) {
                    var arrTmp = new Array(padLeft + 1);
                    var padding = arrTmp.join(pad ? pad : " ");
                } else {
                    var padding = "";
                }
            }
            result += leftpart + padding + subst;
            str = rightPart;
        }
        result += str;
        return result;
    };

    return _sprintf.apply(null, arguments);
};


//========================= Date formatting ================================
//
// Function: Date.strftime
// Description:
//    This is a third generation implementation. This is a JavaScript
//    implementation of C the library function 'strftime'. It supports all
//    format specifiers except U, W, z, Z, G, g, O, E, and V.
//    For a full description of this function, go here:
//       http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html
//    Donating implementations can be found here:
//      http://redhanded.hobix.com/inspect/showingPerfectTime.html
//    and here:
//      http://wiki.osafoundation.org/bin/view/Documentation/JavaScriptStrftime
//  Input:  the date object and the format specification
//  Return: a formatted string
//
//  Example: var date = new Date(); alert(date.strftime("%Y-%m-%d"));
//
// Object Method
Date.prototype.strftime = function (fmt) {
    return Date.strftime(this, fmt);
};

// Class Function
Date.strftime = function (date, fmt) {
    var t = date;
    var cnvts = Date.prototype.strftime._cnvt;
    var str = fmt;
    var m;
    var rex = /([^%]*)%([%aAbBcCdDehHIjmMprRStTuwxXyYZ]{1})(.*)/;

    var result = '';
    while (m = rex.exec(str)) {
        var pre = m[1];
        var typ = m[2];
        var post = m[3];
        result += pre + cnvts[typ](t);
        str = post;
    }
    result += str;
    return result;
};

// the specifier conversion function table
Date.prototype.strftime._cnvt = {
    zeropad: function (n) { return n > 9 ? n : '0' + n; },
    spacepad: function (n) { return n > 9 ? n : ' ' + n; },
    ytd: function (t) {
        var first = new Date(t.getFullYear(), 0, 1).getTime();
        var diff = t.getTime() - first;
        return parseInt(((((diff / 1000) / 60) / 60) / 24)) + 1;
    },
    a: function (t) {
        return [ZStrs.Sun, ZStrs.Mon, ZStrs.Tue, ZStrs.Wed, ZStrs.Thu,
        ZStrs.Fri, ZStrs.Sat][t.getDay()];
    },
    A: function (t) {
        return [ZStrs.Sunday, ZStrs.Monday, ZStrs.Tuesdsay, ZStrs.Wednesday,
        ZStrs.Thursday, ZStrs.Friday,
        ZStrs.Saturday][t.getDay()];
    },
    b: function (t) {
        return [ZStrs.Jan, ZStrs.Feb, ZStrs.Mar, ZStrs.Apr, ZStrs.May, ZStrs.Jun,
        ZStrs.Jul, ZStrs.Aug, ZStrs.Sep, ZStrs.Oct,
        ZStrs.Nov, ZStrs.Dec][t.getMonth()];
    },
    B: function (t) {
        return [ZStrs.January, ZStrs.February, ZStrs.March, ZStrs.April, ZStrs.May,
        ZStrs.June, ZStrs.July, ZStrs.August,
        ZStrs.September, ZStrs.October, ZStrs.November,
        ZStrs.December][t.getMonth()];
    },
    c: function (t) {
        return (this.a(t) + ' ' + this.b(t) + ' ' + this.e(t) + ' ' +
            this.H(t) + ':' + this.M(t) + ':' + this.S(t) + ' ' + this.Y(t));
    },
    C: function (t) { return this.Y(t).slice(0, 2); },
    d: function (t) { return this.zeropad(t.getDate()); },
    D: function (t) { return this.m(t) + '/' + this.d(t) + '/' + this.y(t); },
    e: function (t) { return this.spacepad(t.getDate()); },
    // E: function(t) { return '-' },
    F: function (t) { return this.Y(t) + '-' + this.m(t) + '-' + this.d(t); },
    g: function (t) { return '-'; },
    G: function (t) { return '-'; },
    h: function (t) { return this.b(t); },
    H: function (t) { return this.zeropad(t.getHours()); },
    I: function (t) {
        var s = this.zeropad((t.getHours() + 12) % 12);
        return (s == "00") ? "12" : s;
    },
    j: function (t) { return this.ytd(t); },
    k: function (t) { return this.spacepad(t.getHours()); },
    l: function (t) {
        var s = this.spacepad((t.getHours() + 12) % 12);
        return (s == " 0") ? "12" : s;
    },
    m: function (t) { return this.zeropad(t.getMonth() + 1); }, // month-1
    M: function (t) { return this.zeropad(t.getMinutes()); },
    n: function (t) { return '\n'; },
    // O: function(t) { return '-' },
    p: function (t) { return this.H(t) < 12 ? ZStrs.AM : ZStrs.PM; },
    r: function (t) {
        return this.I(t) + ':' + this.M(t) + ':' + this.S(t) + ' ' + this.p(t);
    },
    R: function (t) { return this.H(t) + ':' + this.M(t); },
    S: function (t) { return this.zeropad(t.getSeconds()); },
    t: function (t) { return '\t'; },
    T: function (t) {
        return this.H(t) + ':' + this.M(t) + ':' + this.S(t) + ' ' + this.p(t);
    },
    u: function (t) { return t.getDay() ? t.getDay() + 1 : 7; },
    U: function (t) { return '-'; },
    w: function (t) { return t.getDay(); }, // 0..6 == sun..sat
    W: function (t) { return '-'; },       // not available
    x: function (t) { return this.D(t); },
    X: function (t) { return this.T(t); },
    y: function (t) { return this.zeropad(this.Y(t) % 100); },
    Y: function (t) { return t.getFullYear().toString(); },
    z: function (t) { return ''; },
    Z: function (t) { return ''; },
    '%': function (t) { return '%'; }
};

// this needs to be worked on...
function _weekNumber(date) {
    var ytd = toNumber(date.strftime("%j"));
    var week = Math.floor(ytd / 7);
    if (new Date(date.getFullYear(), 0, 1).getDay() < 4) {
        week++;
    }
    return week;
};

//
// Format a Date object into a proper ISO 8601 date string
//
psx.toISODateString = function (date, timeDesignator, dateOnly, precision) {
    if (!date) date = new Date();
    var str = '';
    if (timeDesignator == undefined) { timeDesignator = 'T'; };
    function _zeroPad(val) { return (val < 10) ? '0' + val : val; }
    if (date instanceof Date) {
        str = (date.getFullYear() + '-' +
            _zeroPad(date.getMonth() + 1, 2) + '-' +
            _zeroPad(date.getDate(), 2));
        if (!dateOnly) {
            str += (timeDesignator +
                _zeroPad(date.getHours(), 2) + ':' +
                _zeroPad(date.getMinutes(), 2) + ':' +
                _zeroPad(date.getSeconds(), 2));
            if (precision && typeof (precision) == "number") {
                var ms = date.getMilliseconds();
                if (ms) {
                    var millis = _zeroPad(ms.toString(), precision);
                    var s = millis.slice(0, Math.min(precision, millis.length));
                    str += "." + s;
                }
            }
        }
    }
    return str;
};

//
// Make it a Date object method
//
Date.prototype.toISODateString = function (timeDesignator, dateOnly, precision) {
    return psx.toISODateString(this, timeDesignator, dateOnly, precision);
};

// some ISO8601 formats
Date.strftime.iso8601_date = "%Y-%m-%d";
Date.strftime.iso8601_full = "%Y-%m-%dT%H:%M:%S";
Date.strftime.iso8601 = "%Y-%m-%d %H:%M:%S";
Date.strftime.iso8601_time = "%H:%M:%S";

Date.prototype.toISO = function () {
    return this.strftime(Date.strftime.iso8601);
};

Date.prototype.toISOString = Date.prototype.toISODateString;


//
//============================ Array  Extensions ===========================
//

//
// Function: Array.contains
// Description: Determines if an array contains a specific element
// Input:  the array and the element to search for
// Return: true if the element is found, false if not
//
Array.contains = function (ar, el) {
    for (var i = 0; i < ar.length; i++) {
        if (ar[i] == el) {
            return true;
        }
    }
    return false;
};
if (!Array.prototype.contains) {
    // define the instance method
    Array.prototype.contains = function (el) {
        for (var i = 0; i < this.length; i++) {
            if (this[i] == el) {
                return true;
            }
        }
        return false;
    };
}

//
// Function: Array.indexOf
// Description: Determines the index of an element in an array
// Input:  the array and the element to search for
// Return: the index of the element or -1 if not found
//
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function (el) {
        for (var i = 0; i < this.length; i++) {
            if (this[i] == el) {
                return i;
            }
        }
        return -1;
    };
}

//
// Function: Array.lastIndexOf
// Description: Determines the last index of an element in an array
// Input:  the array and the element to search for
// Return: the last index of the element or -1 if not found
//
if (!Array.prototype.lastIndexOf) {
    Array.prototype.indexOf = function (el) {
        for (var i = this.length - 1; i >= 0; i--) {
            if (this[i] == el) {
                return i;
            }
        }
        return -1;
    };
}


//
// Class: Timer
// Description: a simple timer with start and stop methods
//
Timer = function () {
    var self = this;
    self.startTime = 0;
    self.stopTime = 0;
    self.elapsed = 0;
    self.cummulative = 0;
    self.count = 0;
};

Timer.prototype.start = function () {
    this.startTime = new Date().getTime();
};
Timer.prototype.stop = function () {
    var self = this;
    self.stopTime = new Date().getTime();
    self.elapsed = (self.stopTime - self.startTime) / 1000.00;
    self.cummulative += self.elapsed;
    self.count++;
    self.per = self.cummulative / self.count;
};


//
//======================== File and Folder ===================================
//
// Function: toUIString
// Description: the name of a File/Folder suitable for use in a UI
// Input:  <none>
// Return: the formatted file name
//
File.prototype.toUIString = function () {
    return decodeURI(this.fsName);
};
Folder.prototype.toUIString = function () {
    return decodeURI(this.fsName);
};

//========================= Filename formatting ===============================
//
// Function: strf
// Input: File/Folder and the format string
// File.strf(fmt [, fs])
// Folder.strf(fmt [, fs])
//   This is based of the file name formatting facility in exiftool. Part of
//   the description is copied directly from there. You can find exiftool at:
//      http://www.sno.phy.queensu.ca/~phil/exiftool/
//
// Description:
//   Format a file string using a printf-like format string
//
// fmt is a string where the following substitutions occur
//   %d - the directory name (no trailing /)
//   %f - the file name without the extension
//   %e - the file extension without the leading '.'
//   %p - the name of the parent folder
//   %% - the '%' character
//
// if fs is true the folder is in local file system format
//   (e.g. C:\images instead of /c/images)
//
// Examples:
//
// Reformat the file name:
// var f = new File("/c/work/test.jpg");
// f.strf("%d/%f_%e.txt") == "/c/work/test_jpg.txt"
//
// Change the file extension
// f.strf("%d/%f.psd") == "/c/work/test.psd"
//
// Convert to a file name in a subdirectory named after the extension
// f.strf("%d/%e/%f.%e") == "/c/work/jpg/test.jpg"
//
// Change the file extension and convert to a file name in a subdirectory named
//   after the new extension
// f.strf("%d/psd/%f.psd") == "/c/work/psd/test.psd"
//
// var f = new File("~/.bashrc");
// f.strf("%f") == ".bashrc"
// f.strf("%e") == ""
//
// Advanced Substitution
//   A substring of the original file name, directory or extension may be
//   taken by specifying a string length immediately following the % character.
//   If the length is negative, the substring is taken from the end. The
//   substring position (characters to ignore at the start or end of the
//   string) may be given by a second optional value after a decimal point.
// For example:
//
// var f = new File("Picture-123.jpg");
//
// f.strf("%7f.psd") == "Picture.psd"
// f.strf("%-.4f.psd") == "Picture.psd"
// f.strf("%7f.%-3f") == "Picture.123"
// f.strf("Meta%-3.1f.xmp") == "Meta12.xmp"
//
File.prototype.strf = function (fmt, fs) {
    var self = this;
    var name = decodeURI(self.name);
    //var name = (self.name);

    // get the portions of the full path name

    // extension
    var m = name.match(/.+\.([^\.\/]+)$/);
    var e = m ? m[1] : '';

    // basename
    m = name.match(/(.+)\.[^\.\/]+$/);
    var f = m ? m[1] : name;

    fs |= !($.os.match(/windows/i)); // fs only matters on Windows
    // fs |= isMac();

    // full path...
    var d = decodeURI((fs ? self.parent.fsName : self.parent.absoluteURI));

    // parent directory...
    var p = decodeURI(self.parent.name);

    //var d = ((fs ? self.parent.fsName : self.parent.toString()));

    var str = fmt;

    // a regexp for the format specifiers

    var rex = /([^%]*)%(-)?(\d+)?(\.\d+)?(%|d|e|f|p)(.*)/;

    var result = '';

    while (m = rex.exec(str)) {
        var pre = m[1];
        var sig = m[2];
        var len = m[3];
        var ign = m[4];
        var typ = m[5];
        var post = m[6];

        var subst = '';

        if (typ == '%') {
            subst = '%';
        } else {
            var s = '';
            switch (typ) {
                case 'd': s = d; break;
                case 'e': s = e; break;
                case 'f': s = f; break;
                case 'p': s = p; break;
                // default: s = "%" + typ; break; // let others pass through
            }

            var strlen = s.length;

            if (strlen && (len || ign)) {
                ign = (ign ? Number(ign.slice(1)) : 0);
                if (len) {
                    len = Number(len);
                    if (sig) {
                        var _idx = strlen - len - ign;
                        subst = s.slice(_idx, _idx + len);
                    } else {
                        subst = s.slice(ign, ign + len);
                    }
                } else {
                    if (sig) {
                        subst = s.slice(0, strlen - ign);
                    } else {
                        subst = s.slice(ign);
                    }
                }

            } else {
                subst = s;
            }
        }

        result += pre + subst;
        str = post;
    }

    result += str;

    return result;
};
Folder.prototype.strf = File.prototype.strf;


//=========================== PS Functions ===============================

//
// Function: getApplicationProperty
// Description: Get a value from the Application descriptor
// Input:  key - an ID
// Return: The value or undefined
//
psx.getApplicationProperty = function (key) {
    var ref = ref = new ActionReference();
    ref.putProperty(cTID("Prpr"), key);
    ref.putEnumerated(cTID('capp'), cTID("Ordn"), cTID("Trgt"));
    var desc;
    try {
        desc = executeActionGet(ref);
    } catch (e) {
        return undefined;
    }
    var val = undefined;
    if (desc.hasKey(key)) {
        var typ = desc.getType(key);
        switch (typ) {
            case DescValueType.ALIASTYPE:
                val = desc.getPath(key); break;
            case DescValueType.BOOLEANTYPE:
                val = desc.getBoolean(key); break;
            case DescValueType.CLASSTYPE:
                val = desc.getClass(key); break;
            case DescValueType.DOUBLETYPE:
                val = desc.getDouble(key); break;
            case DescValueType.ENUMERATEDTYPE:
                val = desc.getEnumeratedValue(key); break;
            case DescValueType.INTEGERTYPE:
                val = desc.getInteger(key); break;
            case DescValueType.LISTTYPE:
                val = desc.getList(key); break;
            case DescValueType.OBJECTTYPE:
                val = desc.getObjectValue(key); break;
            case DescValueType.RAWTYPE:
                val = desc.getData(key); break;
            case DescValueType.REFERENCETYPE:
                val = desc.getReference(key); break;
            case DescValueType.STRINGTYPE:
                val = desc.getString(key); break;
            case DescValueType.UNITDOUBLE:
                val = desc.getUnitDoubleValue(key); break;
        }
    }
    return val;
};

//
// Class: ColorProfileNames
// Description: a holder for common color profile names
//
ColorProfileNames = {};
ColorProfileNames.ADOBE_RGB = "Adobe RGB (1998)";
ColorProfileNames.APPLE_RGB = "Apple RGB";
ColorProfileNames.PROPHOTO_RGB = "ProPhoto RGB";
ColorProfileNames.SRGB = "sRGB IEC61966-2.1";
ColorProfileNames.COLORMATCH_RGB = "ColorMatch RGB";
ColorProfileNames.WIDEGAMUT_RGB = "Wide Gamut RGB";

//
// Function: delocalizeProfile
// Description: converts a localized color profile name to a name
//              that can be used in the PS DOM API
// Input:  profile - localized color profile name
// Return: a color profile name in EN
//
psx.delocalizeProfile = function (profile) {
    var p = profile;

    switch (profile) {
        case ZStrs.ProfileAdobeRGB: p = ColorProfileNames.ADOBE_RGB; break;
        case ZStrs.ProfileAppleRGB: p = ColorProfileNames.APPLE_RGB; break;
        case ZStrs.ProfileProPhotoRGB: p = ColorProfileNames.PROPHOTO_RGB; break;
        case ZStrs.ProfileSRGB: p = ColorProfileNames.SRGB; break;
        case ZStrs.ProfileColorMatchRGB: p = ColorProfileNames.COLORMATCH_RGB; break;
        case ZStrs.ProfileWideGamutRGB: p = ColorProfileNames.WIDEGAMUT_RGB; break;
        case ZStrs.ProfileLab: profile = "Lab"; break;
        case ZStrs.ProfileWorkingCMYK: profile = "Working CMYK"; break;
        case ZStrs.ProfileWorkingGray: profile = "Working Gray"; break;
        case ZStrs.ProfileWorkingRGB: profile = "Working RGB"; break;
    }

    return p;
};

//
// Function: convertProfile
// Description: converts a document's color profile
// Input:  doc - a Document
//         profile - a color profile name (possibly localized)
//         flatten - should the document be flattened (opt)
// Return: <none>
//
// tpr, why can't we use the DOM for this call?
psx.convertProfile = function (doc, profile, flatten) {
    profile = profile.replace(/\.icc$/i, '');

    profile = psx.delocalizeProfile(profile);

    function _ftn() {
        var desc = new ActionDescriptor();
        var ref = new ActionReference();
        ref.putEnumerated(cTID('Dcmn'), cTID('Ordn'), cTID('Trgt'));
        desc.putReference(cTID('null'), ref);

        if (profile == 'Working RGB' || profile == 'Working CMYK') {
            var idTargetMode = cTID('TMd ');
            var idTargetModeValue = profile == 'Working RGB' ? cTID('RGBM') : cTID('CMYM');
            desc.putClass(idTargetMode, idTargetModeValue);
        } else {
            desc.putString(cTID('T   '), profile);
        }
        desc.putEnumerated(cTID('Inte'), cTID('Inte'), cTID('Clrm'));
        desc.putBoolean(cTID('MpBl'), true);
        desc.putBoolean(cTID('Dthr'), false);
        desc.putInteger(cTID('sdwM'), -1);
        if (flatten != undefined) {
            desc.putBoolean(cTID('Fltt'), !!flatten);
        }
        executeAction(sTID('convertToProfile'), desc, DialogModes.NO);
    }

    _ftn();
};


//
// Function: getDocumentDescriptor
// Description: Gets the ActionDescriptor of a Document
// Input:  doc - a Document
// Return: an ActionDescriptor
//
psx.getDocumentDescriptor = function (doc) {
    var active = undefined;
    if (doc && doc != app.activeDocument) {
        active = app.activeDocument;
        app.activeDocument = doc;
    }
    var ref = new ActionReference();
    ref.putEnumerated(cTID("Dcmn"),
        cTID("Ordn"),
        cTID("Trgt"));  //activeDoc
    var desc = executeActionGet(ref);

    if (active) {
        app.activeDocument = active;
    }

    return desc;
};

//
// Function: getDocumentIndex
// Description: Gets the index of a Document in app.documents
// Input:  doc - a Document
// Return: The index of the document or -1 if not found
//
psx.getDocumentIndex = function (doc) {
    var docs = app.documents;
    for (var i = 0; i < docs.length; i++) {
        if (docs[i] == doc) {
            return i + 1;
        }
    }

    return -1;

    //   return psx.getDocumentDescriptor(doc).getInteger(cTID('ItmI'));
};

//
// Function: revertDocument
// Description: Reverts a document to it's original state
// Input:  doc - a Document
// Return: <none>
//
psx.revertDocument = function (doc) {
    psx.doEvent(doc, "Rvrt");
};


//
// Function: getXMPValue
// Description: Get the XMP value for (tag) from the object (obj).
//              obj can be a String, XML, or Document. Support for
//              Files will be added later.
//              Based on getXMPTagFromXML from Adobe's StackSupport.jsx
// Input:  obj - an object containing XMP data
//         tag - the name of an XMP field
// Return: the value of an XMP field as a String
//
psx.getXMPValue = function (obj, tag) {
    var xmp;

    if (obj.constructor == String) {
        xmp = new XML(obj);

    } else if (obj.typename == "Document") {
        xmp = new XML(obj.xmpMetadata.rawData);

    } else if (obj instanceof XML) {
        xmp = obj;

        // } else if (obj instanceof File) {
        // add support for Files

    } else {
        Error.runtimeError(19, "obj");
    }

    var s;

    // Ugly special case
    if (tag == "ISOSpeedRatings") {
        s = String(xmp.*:: RDF.*:: Description.*:: ISOSpeedRatings.*:: Seq.*:: li);

    } else {
        s = String(eval("xmp.*::RDF.*::Description.*::" + tag));
    }

    return s;
};


//
// Function: getDocumentName
// Description: Gets the name of the document. Doing it this way
//              avoids the side effect recomputing the histogram.
// Input:  doc - a Document
// Return: the name of the Document
//
psx.getDocumentName = function (doc) {
    function _ftn() {
        var ref = new ActionReference();
        ref.putProperty(cTID('Prpr'), cTID('FilR'));
        ref.putEnumerated(cTID('Dcmn'), cTID('Ordn'), cTID('Trgt'));
        var desc = executeActionGet(ref);
        return desc.hasKey(cTID('FilR')) ? desc.getPath(cTID('FilR')) : undefined;
    }
    return _ftn();
};

//
// Function: hasBackgruond
// Description: Determines if a Document has a background
// Input:  doc - a Document
// Return: true if the document has a background, false if not
//
psx.hasBackground = function (doc) {
    return doc.layers[doc.layers.length - 1].isBackgroundLayer;
};

//
// Function: copyLayerToDocument
// Description: Copies a layer from on Document to another
// Input:  doc   - a Document
//         layer - a Layer
//         otherDocument - a Document
// Return: <none>
//
psx.copyLayerToDocument = function (doc, layer, otherDoc) {
    var desc = new ActionDescriptor();
    var fref = new ActionReference();
    fref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
    desc.putReference(cTID('null'), fref);
    var tref = new ActionReference();
    tref.putIndex(cTID('Dcmn'), psx.getDocumentIndex(otherDoc));
    // tref.putName(cTID('Dcmn'), otherDoc.name);
    desc.putReference(cTID('T   '), tref);
    desc.putInteger(cTID('Vrsn'), 2);
    executeAction(cTID('Dplc'), desc, DialogModes.NO);
};

//
// Function: getLayerDescriptor
// Description: Gets the ActionDescriptor for a layer
// Input:  doc   - a Document
//         layer - a Layer
// Return: an ActionDescriptor
//
psx.getLayerDescriptor = function (doc, layer) {
    var ref = new ActionReference();
    ref.putEnumerated(cTID("Lyr "), cTID("Ordn"), cTID("Trgt"));
    return executeActionGet(ref);
};

//
// Function: createLayerMask
// Description: Creates a layer mask for a layer optionally from the
//              current selection
// Input:  doc   - a Document
//         layer - a Layer
//         fromSelection - should mask be made from the current selection (opt)
// Return: <none>
//
psx.createLayerMask = function (doc, layer, fromSelection) {
    var desc = new ActionDescriptor();
    desc.putClass(cTID("Nw  "), cTID("Chnl"));
    var ref = new ActionReference();
    ref.putEnumerated(cTID("Chnl"), cTID("Chnl"), cTID("Msk "));
    desc.putReference(cTID("At  "), ref);
    if (fromSelection == true) {
        desc.putEnumerated(cTID("Usng"), cTID("UsrM"), cTID("RvlS"));
    } else {
        desc.putEnumerated(cTID("Usng"), cTID("UsrM"), cTID("RvlA"));
    }
    executeAction(cTID("Mk  "), desc, DialogModes.NO);
};

//
// Function: hasLayerMask
//           isLayerMaskEnabled
//           disableLayerMask
//           enableLayerMask
//           setLayerMaskEnabledState
// Description: A collection of functions dealing with the state
//              of a layer's mask.
//              
// Input:  doc   - a Document
//         layer - a Layer
// Return: boolean or <none>
//
psx.hasLayerMask = function (doc, layer) {
    return psx.getLayerDescriptor().hasKey(cTID("UsrM"));
};
psx.isLayerMaskEnabled = function (doc, layer) {
    var desc = psx.getLayerDescriptor(doc, layer);
    return (desc.hasKey(cTID("UsrM")) && desc.getBoolean(cTID("UsrM")));
};
psx.disableLayerMask = function (doc, layer) {
    psx.setLayerMaskEnabledState(doc, layer, false);
};
psx.enableLayerMask = function (doc, layer) {
    psx.setLayerMaskEnabledState(doc, layer, true);
};
psx.setLayerMaskEnabledState = function (doc, layer, state) {
    if (state == undefined) {
        state = false;
    }
    var desc = new ActionDescriptor();

    var ref = new ActionReference();
    ref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
    desc.putReference(cTID('null'), ref);

    var tdesc = new ActionDescriptor();
    tdesc.putBoolean(cTID('UsrM'), state);
    desc.putObject(cTID('T   '), cTID('Lyr '), tdesc);

    executeAction(cTID('setd'), desc, DialogModes.NO);
};

//
// Function: mergeVisible
//           mergeSelected
//           mergeDown
//           mergeAllLayers
// Description: A collection of functions for merging layers
// Input:  doc - a Document
// Return: <none>
//
psx.mergeVisible = function (doc) {
    psx.doEvent(doc, "MrgV");  // "MergeVisible"
};
psx.mergeSelected = function (doc) {
    psx.doEvent(doc, "Mrg2");  // "MergeLayers"
};
psx.mergeDown = function (doc) {
    psx.doEvent(doc, "Mrg2");  // "MergeLayers"
};
psx.mergeAllLayers = function (doc) {
    psx.selectAllLayers(doc);

    if (psx.hasBackground(doc)) {
        psx.appendLayerToSelectionByName(doc, doc.backgroundLayer.name);
    }
    psx.mergeSelected(doc);
};

//
// Function: appendLayerToSelectionByName
// Description: Adds a layer to the current set of selected layers
// Input:  doc  - a Document
//         name - a layer's name
// Return: <none>
//
psx.appendLayerToSelectionByName = function (doc, name) {
    var desc25 = new ActionDescriptor();
    var ref18 = new ActionReference();
    ref18.putName(cTID('Lyr '), name);
    desc25.putReference(cTID('null'), ref18);
    desc25.putEnumerated(sTID('selectionModifier'),
        sTID('selectionModifierType'),
        sTID('addToSelection'));
    desc25.putBoolean(cTID('MkVs'), false);
    executeAction(cTID('slct'), desc25, DialogModes.NO);
};

//
// Function: selectAllLayers
// Description: Select all layers in a document
// Input:  doc  - a Document
// Return: <none>
//
psx.selectAllLayers = function (doc) {
    var desc18 = new ActionDescriptor();
    var ref11 = new ActionReference();
    ref11.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
    desc18.putReference(cTID('null'), ref11);
    executeAction(sTID('selectAllLayers'), desc18, DialogModes.NO);
};

//
// Function: getLayerBounds
// Description: Gets the bounds of the layer content (without the mask)
// Input:  doc  - a Document
//         layer - a Layer
// Return: the bounding rectangle of the layer's content
//
psx.getLayerBounds = function (doc, layer) {
    var ru = app.preferences.rulerUnits;

    var reenable = false;
    var st;
    if (psx.hasLayerMask(doc, layer) &&
        psx.isLayerMaskEnabled(doc, layer)) {
        st = doc.activeHistoryState;
        psx.disableLayerMask(doc, layer);
        reenable = true;
    }

    var lbnds = layer.bounds;

    // fix this to modify the history state
    if (reenable) {
        psx.enableLayerMask(doc, layer);
    }
    for (var i = 0; i < 4; i++) {
        lbnds[i] = lbnds[i].as("px");
    }

    return lbnds;
};

//
// Function: selectBounds
// Description: Create a selection using the given bounds
// Input:  doc  - a Document
//         b - bounding rectangle (in pixels)
//         type - the selection type
//         feather - the amount of feather (opt: 0)
//         antialias - should antialias be used (opt: false)
// Return: <none>
//
psx.selectBounds = function (doc, b, type, feather, antialias) {
    if (feather == undefined) {
        feather = 0;
    }

    if (antialias == undefined) {
        antialias = false;
    }

    doc.selection.select([[b[0], b[1]],
    [b[2], b[1]],
    [b[2], b[3]],
    [b[0], b[3]]],
        type, feather, antialias);
};

//
// Function: getSelectionBounds
// Description: Get the bounds of the current selection
// Input:  doc  - a Document
// Return: a bound rectangle (in pixels)
//
psx.getSelectionBounds = function (doc) {
    var bnds = [];
    var sbnds = doc.selection.bounds;
    for (var i = 0; i < sbnds.length; i++) {
        bnds[i] = sbnds[i].as("px");
    }
    return bnds;
};

//
// Function: hasSelection
// Input:  doc  - a Document
// Return: returns true if the document has as selection, false if not
//
psx.hasSelection = function (doc) {
    var res = false;

    var as = doc.activeHistoryState;
    doc.selection.deselect();
    if (as != doc.activeHistoryState) {
        res = true;
        doc.activeHistoryState = as;
    }

    return res;
};

//
// Function: rgbToString
// Description: Convert a SolidColor into a RGB string
// Input:  c - SolidColor
// Return: an RGB string (e.g. "[128,255,128]")
//
psx.rgbToString = function (c) {
    return "[" + c.rgb.red + "," + c.rgb.green + "," + c.rgb.blue + "]";
};
//
// Function: rgbToArray
// Description: Convert a SolidColor into a RGB array
// Input:  c - SolidColor
// Return: an RGB array (e.g. [128,255,128])
//
psx.rgbToArray = function (c) {
    return [c.rgb.red, c.rgb.green, c.rgb.blue];
};
//
// Function: rgbFromString
// Description: Converts an RBG string to a SolidColor
// Input:  str - an RGB string
// Return: a SolidColor
//
psx.rgbFromString = function (str) {
    var rex = /([\d\.]+),([\d\.]+),([\d\.]+)/;
    var m = str.match(rex);
    if (m) {
        return psx.createRGBColor(Number(m[1]),
            Number(m[2]),
            Number(m[3]));
    }
    return undefined;
};
//
// Function: createRGBColor
// Description: Creates a SolidColor from RGB values
// Input:  r - red
//         g - green
//         b - blue
// Return: a SolidColor
//
psx.createRGBColor = function (r, g, b) {
    var c = new RGBColor();
    if (r instanceof Array) {
        b = r[2]; g = r[1]; r = r[0];
    }
    c.red = parseInt(r); c.green = parseInt(g); c.blue = parseInt(b);
    var sc = new SolidColor();
    sc.rgb = c;
    return sc;
};

//
// Predefine some common colors
//
psx.COLOR_BLACK = psx.createRGBColor(0, 0, 0);
psx.COLOR_RED = psx.createRGBColor(255, 0, 0);
psx.COLOR_GREEN = psx.createRGBColor(0, 255, 0);
psx.COLOR_BLUE = psx.createRGBColor(0, 0, 255);
psx.COLOR_GRAY = psx.createRGBColor(128, 128, 128);
psx.COLOR_WHITE = psx.createRGBColor(255, 255, 255);

//
// Function: colorFromString
// Description: Creates a SolidColor from an RGBString
// Input:  str - an RGB string
// Return: a SolidColor
//
psx.colorFromString = function (str) {
    var c = psx.rgbFromString(str);
    if (!c) {
        str = str.toLowerCase();
        if (str == ZStrs.black.toLowerCase()) {
            c = psx.COLOR_BLACK;
        } else if (str == ZStrs.white.toLowerCase()) {
            c = psx.COLOR_WHITE;
        } else if (str == ZStrs.foreground.toLowerCase()) {
            c = app.foregroundColor;
        } else if (str == ZStrs.background.toLowerCase()) {
            c = app.backgroundColor;
        } else if (str == ZStrs.gray.toLowerCase() ||
            str == ZStrs.grey.toLowerCase()) {
            c = psx.COLOR_GRAY;
        } else if (str == ZStrs.red.toLowerCase()) {
            c = psx.COLOR_RED;
        } else if (str == ZStrs.green.toLowerCase()) {
            c = psx.COLOR_GREEN;
        } else if (str == ZStrs.blue.toLowerCase()) {
            c = psx.COLOR_BLUE;
        }
    }
    return c;
};


//
// Function: winFileSelection
// Description: Determines if a File is an image file (checks file extension)
// Input:  f - File
// Return: true if f is an image file, false if not
//
psx.winFileSelection = function (f) {
    var suffix = f.name.match(/[.](\w+)$/);
    var t;

    if (suffix && suffix.length == 2) {
        suffix = suffix[1].toUpperCase();
        for (t in app.windowsFileTypes) {
            if (suffix == app.windowsFileTypes[t]) {
                // Ignore mac-generated system thumbnails
                if (f.name.slice(0, 2) != "._") {
                    return true;
                }
            }
        }
    }

    return false;
};

//
// Function: macFileSelection
// Description: Determines if a File is an image file (by file type/extension)
// Input:  f - File
// Return: true if f is an image file, false if not
//
psx.macFileSelection = function (f) {
    var t;
    for (t in app.macintoshFileTypes) {
        if (f.type == app.macintoshFileTypes[t]) {
            return true;
        }
    }

    // Also check windows suffixes...
    return winFileSelection(f);
}


//
// Function: isValidImageFile
// Description: Determines if a File is an image file (by file type/extension)
// Input:  f - File
// Return: true if f is an image file, false if not
//
psx.isValidImageFile = function (f) {
    function _winCheck(f) {
        // skip mac system files
        if (f.name.startsWith("._")) {
            return false;
        }

        var ext = f.strf('%e').toUpperCase();
        return (ext.length > 0) && app.windowsFileTypes.contains(ext);
    }
    function _macCheck(f) {
        return app.macintoshFileTypes.contains(f.type) || _winCheck(f);
    }

    return ((f instanceof File) &&
        (((File.fs == "Macintosh") && _macCheck(f)) ||
            ((File.fs == "Windows") && _winCheck(f))));
};



//============================ Actions =====================================

//
// Function: getActionSets
// Description: Returns the ActionSets in the Actions palette
// Input:  <none>
// Return: an array of objects that have these properties
//         name - the name of the ActionSet
//         actions - an array of the names of the actions in this set
//
psx.getActionSets = function () {
    var i = 1;
    var sets = [];

    while (true) {
        var ref = new ActionReference();
        ref.putIndex(cTID("ASet"), i);
        var desc;
        var lvl = $.level;
        $.level = 0;
        try {
            desc = executeActionGet(ref);
        } catch (e) {
            break;    // all done
        } finally {
            $.level = lvl;
        }
        if (desc.hasKey(cTID("Nm  "))) {
            var set = {};
            set.index = i;
            set.name = desc.getString(cTID("Nm  "));
            set.toString = function () { return this.name; };
            set.count = desc.getInteger(cTID("NmbC"));
            set.actions = [];
            for (var j = 1; j <= set.count; j++) {
                var ref = new ActionReference();
                ref.putIndex(cTID('Actn'), j);
                ref.putIndex(cTID('ASet'), set.index);
                var adesc = executeActionGet(ref);
                var actName = adesc.getString(cTID('Nm  '));
                set.actions.push(actName);
            }
            sets.push(set);
        }
        i++;
    }

    return sets;
};


//============================= ScriptUI stuff =============================

psxui = function () { }

// XXX - Need to check to see if decimalPoint is a special RegEx character
// that needs to be escaped. Currently, we only handle '.'
psxui.dpREStr = (psx.decimalPoint == '.' ? "\\." : psx.decimalPoint);

//
// Function: numberKeystrokeFilter
//           positiveNumberKeystrokeFilter
//           numericKeystrokeFilter
//           unitValueKeystrokeFilter
// Description: A collection of KeyStroke filters that can be used with
//              EditText widgets
// Input:  <none>
// Return: <none>
//
psxui.numberKeystrokeFilter = function () {
    var ftn = psxui.numberKeystrokeFilter;

    if (this.text.match(ftn.matchRE)) {
        if (!ftn.replaceRE) {
            ftn.replaceRE = RegExp(ftn.matchRE.toString().replace(/\//g, ''), "g");
        }
        this.text = this.text.replace(ftn.replaceRE.toString(), '');
    }
};
psxui.numberKeystrokeFilter.matchRE = RegExp("[^\\-" + psxui.dpREStr + "\\d]");

psxui.positiveNumberKeystrokeFilter = function () {
    var ftn = psxui.positiveNumberKeystrokeFilter;

    if (this.text.match(ftn.matchRE)) {
        if (!ftn.replaceRE) {
            ftn.replaceRE = RegExp(ftn.matchRE.toString().replace(/\//g, ''), "g");
        }
        this.text = this.text.replace(ftn.replaceRE, '');
    }
};
psxui.positiveNumberKeystrokeFilter.matchRE =
    RegExp("[^" + psxui.dpREStr + "\\d]");

psxui.numericKeystrokeFilter = function () {
    if (this.text.match(/[^\d]/)) {
        this.text = this.text.replace(/[^\d]/g, '');
    }
};

psxui.unitValueKeystrokeFilter = function () {
    var ftn = psxui.unitValueKeystrokeFilter;

    if (this.text.match(ftn.matchRE)) {
        if (!ftn.replaceRE) {
            ftn.replaceRE = RegExp(ftn.matchRE.toString().replace(/\//g, ''), "g");
        }
        this.text = this.text.toLowerCase().replace(ftn.replaceRE, '');
    }
};
psxui.unitValueKeystrokeFilter.matchRE =
    RegExp("[^\\w% " + psxui.dpREStr + "\\d]");

psxui.unitValueKeystrokeFilter.replaceRE =
    RegExp("[^\\w% " + psxui.dpREStr + "\\d]", "gi");


//
// Function: setMenuSelection
// Description: Select an item on a menu
// Input:  menu - a Menu UI widget
//         txt - text of the menu element
//         def - an element to use if the desired one can't be found
//         ignoreCase - whether or not case insensitive comparison is used
// Return: <none>
//
psxui.setMenuSelection = function (menu, txt, def, ignoreCase) {
    var it = menu.find(txt);
    var last = menu.selection;

    if (!it && ignoreCase) {
        var items = menu.items;
        txt = txt.toLowerCase();
        for (var i = 0; i < items.length; i++) {
            if (txt == items[i].text.toLowerCase()) {
                it = items[i];
                break;
            }
        }
    }

    if (!it) {
        if (def != undefined) {
            var n = toNumber(def);
            if (!isNaN(n)) {
                it = def;

            } else {
                it = menu.find(def);
            }
        }
    }

    if (it != undefined) {
        menu.selection = it;
        menu._last = last;

    } else {
        // XXX debug only...
        if (Folder("/Users/xbytor").exists) {
            $.level = 1; debugger;
        }
        alert("DEBUG: " + txt + " not found in menu");
    }
};

//============================ File Save =====================================
//
// FileSave is only available in Photoshop
//
FileSaveOptions = function (obj) {
    var self = this;

    self.saveDocumentType = undefined; // SaveDocumentType
    self.fileType = "jpg";             // file extension

    self._saveOpts = undefined;

    self.saveForWeb = false; // gif, png, jpg

    self.flattenImage = false; // psd, tif

    self.bmpAlphaChannels = true;
    self.bmpDepth = BMPDepthType.TWENTYFOUR;
    self.bmpRLECompression = false;

    self.gifTransparency = true;
    self.gifInterlaced = false;
    self.gifColors = 256;

    self.jpgQuality = 10;
    self.jpgEmbedColorProfile = true;
    self.jpgFormat = FormatOptions.STANDARDBASELINE;
    self.jpgConvertToSRGB = false;          // requires code

    self.epsEncoding = SaveEncoding.BINARY;
    self.epsEmbedColorProfile = true;

    self.pdfEncoding = PDFEncoding.JPEG;
    self.pdfEmbedColorProfile = true;

    self.psdAlphaChannels = true;
    self.psdEmbedColorProfile = true;
    self.psdLayers = true;
    self.psdMaximizeCompatibility = true;           // requires code for prefs

    self.pngInterlaced = false;

    self.tgaAlphaChannels = true;
    self.tgaRLECompression = true;

    self.tiffEncoding = TIFFEncoding.NONE;
    self.tiffByteOrder = (isWindows() ? ByteOrder.IBM : ByteOrder.MACOS);
    self.tiffEmbedColorProfile = true;

    if (obj) {
        for (var idx in self) {
            if (idx in obj) {       // only copy in FSO settings
                self[idx] = obj[idx];
            }
        }
        if (!obj.fileType) {
            self.fileType = obj.fileSaveType;
            if (self.fileType == "tiff") {
                self.fileType = "tif";
            }
        }
    }
};
//FileSaveOptions.prototype.typename = "FileSaveOptions";
FileSaveOptions._enableDNG = false;

FileSaveOptions.convert = function (fsOpts) {
    var fsType = fsOpts.fileType;
    if (!fsType) {
        fsType = fsOpts.fileSaveType;
    }
    var fs = FileSaveOptionsTypes[fsType];
    if (fs == undefined) {
        return undefined;
    }
    if (!fs.optionsType) {
        return undefined;
    }
    var saveOpts = new fs.optionsType();

    switch (fsType) {
        case "bmp": {
            saveOpts.rleCompression = toBoolean(fsOpts.bmpRLECompression);

            var value = BMPDepthType.TWENTYFOUR;
            var str = fsOpts.bmpDepth.toString();
            if (str.match(/1[^6]|one/i)) {
                value = BMPDepthType.ONE;
            } else if (str.match(/24|twentyfour/i)) {
                // we have to match 24 before 4
                value = BMPDepthType.TWENTYFOUR;
            } else if (str.match(/4|four/i)) {
                value = BMPDepthType.FOUR;
            } else if (str.match(/8|eight/i)) {
                value = BMPDepthType.EIGHT;
            } else if (str.match(/16|sixteen/i)) {
                value = BMPDepthType.SIXTEEN;
            } else if (str.match(/32|thirtytwo/i)) {
                value = BMPDepthType.THIRTYTWO;
            }
            saveOpts.depth = value;
            saveOpts.alphaChannels = toBoolean(fsOpts.bmpAlphaChannels);

            saveOpts._flatten = true;
            saveOpts._8Bit = true; //XXX Should this be true?
            break;
        }
        case "gif": {
            saveOpts.transparency = toBoolean(fsOpts.gifTransparency);
            saveOpts.interlaced = toBoolean(fsOpts.gifInterlaced);
            saveOpts.colors = toNumber(fsOpts.gifColors);

            saveOpts._convertToIndexed = true;
            saveOpts._flatten = true;
            saveOpts._8Bit = true;
            saveOpts._saveForWeb = toBoolean(fsOpts.saveForWeb);
            break;
        }
        case "jpg": {
            saveOpts.quality = toNumber(fsOpts.jpgQuality);
            saveOpts.embedColorProfile = toBoolean(fsOpts.jpgEmbedColorProfile);
            var value = FormatOptions.STANDARDBASELINE;
            var str = fsOpts.jpgFormat.toString();
            if (str.match(/standard/i)) {
                value = FormatOptions.STANDARDBASELINE;
            } else if (str.match(/progressive/i)) {
                value = FormatOptions.PROGRESSIVE;
            } else if (str.match(/optimized/i)) {
                value = FormatOptions.OPTIMIZEDBASELINE;
            }
            saveOpts.formatOptions = value;

            saveOpts._convertToSRGB = toBoolean(fsOpts.jpgConvertToSRGB);
            saveOpts._flatten = true;
            saveOpts._8Bit = true;
            saveOpts._saveForWeb = toBoolean(fsOpts.saveForWeb);
            break;
        }
        case "psd": {
            saveOpts.alphaChannels = toBoolean(fsOpts.psdAlphaChannels);
            saveOpts.embedColorProfile = toBoolean(fsOpts.psdEmbedColorProfile);
            saveOpts.layers = toBoolean(fsOpts.psdLayers);
            saveOpts.maximizeCompatibility =
                toBoolean(fsOpts.psdMaximizeCompatibility);
            saveOpts.flattenImage = toBoolean(fsOpts.flattenImage);
            break;
        }
        case "eps": {
            var value = SaveEncoding.BINARY;
            var str = fsOpts.epsEncoding.toString();
            if (str.match(/ascii/i)) {
                value = SaveEncoding.ASCII;
            } else if (str.match(/binary/i)) {
                value = SaveEncoding.BINARY;
            } else if (str.match(/jpg|jpeg/i)) {
                if (str.match(/high/i)) {
                    value = SaveEncoding.JPEGHIGH;
                } else if (str.match(/low/i)) {
                    value = SaveEncoding.JPEGLOW;
                } else if (str.match(/max/i)) {
                    value = SaveEncoding.JPEGMAXIMUM;
                } else if (str.match(/med/i)) {
                    value = SaveEncoding.JPEGMEDIUM;
                }
            }
            saveOpts.encoding = value;
            saveOpts.embedColorProfile = toBoolean(fsOpts.epsEmbedColorProfile);

            saveOpts._flatten = true;
            break;
        }
        case "pdf": {
            saveOpts.embedColorProfile = toBoolean(fsOpts.pdfEmbedColorProfile);
            break;
        }
        case "png": {
            saveOpts.interlaced = toBoolean(fsOpts.pngInterlaced);

            saveOpts._flatten = true;
            saveOpts._saveForWeb = toBoolean(fsOpts.saveForWeb);
            break;
        }
        case "tga": {
            saveOpts.alphaChannels = toBoolean(fsOpts.tgaAlphaChannels);
            saveOpts.rleCompression = toBoolean(fsOpts.tgaRLECompression);

            saveOpts._flatten = true;
            break;
        }
        case "tif": {
            var value = (isWindows() ? ByteOrder.IBM : ByteOrder.MACOS);
            var str = fsOpts.tiffByteOrder.toString();
            if (str.match(/ibm|pc/i)) {
                value = ByteOrder.IBM;
            } else if (str.match(/mac/i)) {
                value = ByteOrder.MACOS;
            }
            saveOpts.byteOrder = value;

            var value = TIFFEncoding.NONE;
            var str = fsOpts.tiffEncoding.toString();
            if (str.match(/none/i)) {
                value = TIFFEncoding.NONE;
            } else if (str.match(/lzw/i)) {
                value = TIFFEncoding.TIFFLZW;
            } else if (str.match(/zip/i)) {
                value = TIFFEncoding.TIFFZIP;
            } else if (str.match(/jpg|jpeg/i)) {
                value = TIFFEncoding.JPEG;
            }
            saveOpts.imageCompression = value;

            saveOpts.embedColorProfile = toBoolean(fsOpts.tiffEmbedColorProfile);
            saveOpts.flattenImage = toBoolean(fsOpts.flattenImage);
            break;
        }
        case "dng": {
        }
        default: {
            Error.runtimeError(9001, "Internal Error: Unknown file type: " +
                fs.fileType);
        }
    }

    return saveOpts;
};

FileSaveOptionsType = function (fileType, menu, saveType, optionsType) {
    var self = this;

    self.fileType = fileType;    // the file extension
    self.menu = menu;
    self.saveType = saveType;
    self.optionsType = optionsType;
};
FileSaveOptionsType.prototype.typename = "FileSaveOptionsType";

FileSaveOptionsTypes = [];
FileSaveOptionsTypes._add = function (fileType, menu, saveType, optionsType) {
    var fsot = new FileSaveOptionsType(fileType, menu, saveType, optionsType);
    FileSaveOptionsTypes.push(fsot);
    FileSaveOptionsTypes[fileType] = fsot;
};
FileSaveOptionsTypes._init = function () {
    if (!isPhotoshop()) {
        return;
    }
    FileSaveOptionsTypes._add("bmp", "Bitmap (BMP)", SaveDocumentType.BMP,
        BMPSaveOptions);
    FileSaveOptionsTypes._add("gif", "GIF", SaveDocumentType.COMPUSERVEGIF,
        GIFSaveOptions);
    FileSaveOptionsTypes._add("jpg", "JPEG", SaveDocumentType.JPEG,
        JPEGSaveOptions);
    FileSaveOptionsTypes._add("psd", "Photoshop PSD", SaveDocumentType.PHOTOSHOP,
        PhotoshopSaveOptions);
    FileSaveOptionsTypes._add("eps", "Photoshop EPS",
        SaveDocumentType.PHOTOSHOPEPS, EPSSaveOptions);
    FileSaveOptionsTypes._add("pdf", "Photoshop PDF",
        SaveDocumentType.PHOTOSHOPPDF, PDFSaveOptions);
    FileSaveOptionsTypes._add("png", "PNG", SaveDocumentType.PNG,
        PNGSaveOptions);
    FileSaveOptionsTypes._add("tga", "Targa", SaveDocumentType.TARGA,
        TargaSaveOptions);
    FileSaveOptionsTypes._add("tif", "TIFF", SaveDocumentType.TIFF,
        TiffSaveOptions);

    if (FileSaveOptions._enableDNG) {
        FileSaveOptionsTypes._add("dng", "DNG", undefined, undefined);
    }
};
FileSaveOptionsTypes._init();

// XXX remove file types _before_ creating a FS panel!
FileSaveOptionsTypes.remove = function (ext) {
    var ar = FileSaveOptionsTypes;
    var fsot = ar[ext];
    if (fsot) {
        for (var i = 0; i < ar.length; i++) {
            if (ar[i] == fsot) {
                ar.splice(i, 1);
                break;
            }
        }
        delete ar[ext];
    }
};

FileSaveOptions.createFileSavePanel = function (pnl, ini) {
    var win = pnl.window;
    pnl.mgr = this;

    var menuElements = [];

    for (var i = 0; i < FileSaveOptionsTypes.length; i++) {
        menuElements.push(FileSaveOptionsTypes[i].menu);
    }

    var w = pnl.bounds[2] - pnl.bounds[0];
    var xofs = 0;
    var y = 0;

    var opts = new FileSaveOptions(ini);

    if (pnl.type == 'panel') {
        xofs += 5;
        y += 10;
    }
    pnl.text = "Save Options";

    var tOfs = 3;

    var x = xofs;
    // tpr needs zstring
    var w = pnl.add('statictext', [x, y + tOfs, x + 60, y + 22 + tOfs], 'File Type:');
    x += w.bounds.width + (isWindows() ? 5 : 15);
    pnl.fileType = pnl.add('dropdownlist', [x, y, x + 150, y + 22], menuElements);

    var ftype = opts.fileType || opts.fileSaveType || "jpg";

    var ft = psx.getByProperty(FileSaveOptionsTypes,
        "fileType",
        ftype);
    pnl.fileType.selection = pnl.fileType.find(ft.menu);

    x += pnl.fileType.bounds.width + 10;
    // tpr needs zstring
    pnl.saveForWeb = pnl.add('checkbox', [x, y, x + 135, y + 22], 'Save for Web');
    pnl.saveForWeb.visible = false;
    pnl.saveForWeb.value = false;
    // tpr needs zstring
    pnl.flattenImage = pnl.add('checkbox', [x, y, x + 135, y + 22], 'Flatten Image');
    pnl.flattenImage.visible = false;
    pnl.flattenImage.value = false;

    y += 30;
    var yofs = y;

    x = xofs;

    //=============================== Bitmap ===============================
    if (FileSaveOptionsTypes["bmp"]) {
        pnl.bmpAlphaChannels = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Alpha Channels");

        x += 150;
        var bmpDepthMenu = ["1", "4", "8", "16", "24", "32"];
        // tpr needs zstring
        pnl.bmpDepthLabel = pnl.add('statictext', [x, y + tOfs, x + 60, y + 22 + tOfs],
            'Bit Depth:');
        x += 65;
        pnl.bmpDepth = pnl.add('dropdownlist', [x, y, x + 55, y + 22], bmpDepthMenu);
        pnl.bmpDepth.selection = pnl.bmpDepth.find("24");

        pnl.bmpDepth.find("1")._value = BMPDepthType.ONE;
        pnl.bmpDepth.find("4")._value = BMPDepthType.FOUR;
        pnl.bmpDepth.find("8")._value = BMPDepthType.EIGHT;
        pnl.bmpDepth.find("16")._value = BMPDepthType.SIXTEEN;
        pnl.bmpDepth.find("24")._value = BMPDepthType.TWENTYFOUR;
        pnl.bmpDepth.find("32")._value = BMPDepthType.THIRTYTWO;

        x = xofs;
        y += 30;
        // tpr needs zstring
        pnl.bmpRLECompression = pnl.add('checkbox', [x, y, x + 145, y + 22],
            "RLE Compression");

        pnl.bmp = ["bmpAlphaChannels", "bmpDepthLabel", "bmpDepth",
            "bmpRLECompression"];

        pnl.bmpAlphaChannels.value = toBoolean(opts.bmpAlphaChannels);
        var it = pnl.bmpDepth.find(opts.bmpDepth.toString());
        if (it) {
            pnl.bmpDepth.selection = it;
        }
        pnl.bmpRLECompression.value = toBoolean(opts.bmpRLECompression);

        y = yofs;
        x = xofs;
    }


    //=============================== GIF ===============================
    if (FileSaveOptionsTypes["gif"]) {
        pnl.gifTransparency = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Transparency");

        x += 125;
        pnl.gifInterlaced = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Interlaced");

        x += 125;
        pnl.gifColorsLabel = pnl.add('statictext', [x, y + tOfs, x + 55, y + 22 + tOfs],
            'Colors:');

        x += 60;
        pnl.gifColors = pnl.add('edittext', [x, y, x + 55, y + 22], "256");
        pnl.gifColors.onChanging = psx.numericKeystrokeFilter;
        pnl.gifColors.onChange = function () {
            var pnl = this.parent;
            var n = toNumber(pnl.gifColors.text || 256);
            if (n < 2) { n = 2; }
            if (n > 256) { n = 256; }
            pnl.gifColors.text = n;
        }

        pnl.gif = ["gifTransparency", "gifInterlaced", "gifColors", "gifColorsLabel",
            "saveForWeb"];

        pnl.gifTransparency.value = toBoolean(opts.gifTransparency);
        pnl.gifInterlaced.value = toBoolean(opts.gifInterlaced);
        pnl.gifColors.text = toNumber(opts.gifColors || 256);

        pnl.saveForWeb.value = toBoolean(opts.saveForWeb);
        y = yofs;
        x = xofs;
    }


    //=============================== JPG ===============================
    if (FileSaveOptionsTypes["jpg"]) {
        pnl.jpgQualityLabel = pnl.add('statictext', [x, y + tOfs, x + 55, y + 22 + tOfs],
            'Quality:');
        x += isWindows() ? 65 : 75;
        var jpqQualityMenu = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
        pnl.jpgQuality = pnl.add('dropdownlist', [x, y, x + 55, y + 22], jpqQualityMenu);
        pnl.jpgQuality.selection = pnl.jpgQuality.find("10");

        y += 30;
        x = xofs;
        pnl.jpgEmbedColorProfile = pnl.add('checkbox', [x, y, x + 155, y + 22],
            "Embed Color Profile");

        y = yofs;
        x += 150;

        var jpgFormatMenu = ["Standard", "Progressive", "Optimized"];
        pnl.jpgFormatLabel = pnl.add('statictext', [x, y + tOfs, x + 50, y + 22 + tOfs],
            'Format:');
        x += 55;
        pnl.jpgFormat = pnl.add('dropdownlist', [x, y, x + 110, y + 22], jpgFormatMenu);
        pnl.jpgFormat.selection = pnl.jpgFormat.find("Standard");

        pnl.jpgFormat.find("Standard")._value = FormatOptions.STANDARDBASELINE;
        pnl.jpgFormat.find("Progressive")._value = FormatOptions.PROGRESSIVE;
        pnl.jpgFormat.find("Optimized")._value = FormatOptions.OPTIMIZEDBASELINE;

        y += 30;
        x = xofs + 150;
        pnl.jpgConvertToSRGB = pnl.add('checkbox', [x, y, x + 145, y + 22],
            "Convert to sRGB");

        pnl.jpg = ["jpgQualityLabel", "jpgQuality", "jpgEmbedColorProfile",
            "jpgFormatLabel", "jpgFormat", "jpgConvertToSRGB", "saveForWeb"];

        var it = pnl.jpgQuality.find(opts.jpgQuality.toString());
        if (it) {
            pnl.jpgQuality.selection = it;
        }
        pnl.jpgEmbedColorProfile.value = toBoolean(opts.jpgEmbedColorProfile);
        var it = pnl.jpgFormat.find(opts.jpgFormat);
        if (it) {
            pnl.jpgFormat.selection = it;
        }
        pnl.jpgConvertToSRGB.value = toBoolean(opts.jpgConvertToSRGB);

        pnl.saveForWeb.value = toBoolean(opts.saveForWeb);

        x = xofs;
        y = yofs;
    }


    //=============================== PSD ===============================
    if (FileSaveOptionsTypes["psd"]) {
        pnl.psdAlphaChannels = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Alpha Channels");

        y += 30;
        pnl.psdEmbedColorProfile = pnl.add('checkbox', [x, y, x + 155, y + 22],
            "Embed Color Profile");

        y = yofs;
        x = xofs + 150;

        pnl.psdLayers = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Layers");

        y += 30;
        pnl.psdMaximizeCompatibility = pnl.add('checkbox', [x, y, x + 175, y + 22],
            "Maximize Compatibility");

        pnl.psd = ["psdAlphaChannels", "psdEmbedColorProfile",
            "psdLayers", "psdMaximizeCompatibility",
            "flattenImage"];

        pnl.psdAlphaChannels.value = toBoolean(opts.psdAlphaChannels);
        pnl.psdEmbedColorProfile.value = toBoolean(opts.psdEmbedColorProfile);
        pnl.psdLayers.value = toBoolean(opts.psdLayers);
        pnl.psdMaximizeCompatibility.value =
            toBoolean(opts.psdMaximizeCompatibility);

        pnl.flattenImage.value = toBoolean(opts.flattenImage);

        x = xofs;
        y = yofs;
    }

    //=============================== EPS ===============================
    if (FileSaveOptionsTypes["eps"]) {
        var epsEncodingMenu = ["ASCII", "Binary", "JPEG High", "JPEG Med",
            "JPEG Low", "JPEG Max"];
        pnl.epsEncodingLabel = pnl.add('statictext', [x, y + tOfs, x + 60, y + 22 + tOfs],
            'Encoding:');
        x += 65;
        pnl.epsEncoding = pnl.add('dropdownlist',
            [x, y, x + 100, y + 22],
            epsEncodingMenu);
        pnl.epsEncoding.selection = pnl.epsEncoding.find("Binary");

        pnl.epsEncoding.find("ASCII")._value = SaveEncoding.ASCII;
        pnl.epsEncoding.find("Binary")._value = SaveEncoding.BINARY;
        pnl.epsEncoding.find("JPEG High")._value = SaveEncoding.JPEGHIGH;
        pnl.epsEncoding.find("JPEG Low")._value = SaveEncoding.JPEGLOW;
        pnl.epsEncoding.find("JPEG Max")._value = SaveEncoding.JPEGMAXIMUM;
        pnl.epsEncoding.find("JPEG Med")._value = SaveEncoding.JPEGMEDIUM;

        x = xofs;
        y += 30;
        pnl.epsEmbedColorProfile = pnl.add('checkbox', [x, y, x + 155, y + 22],
            "Embed Color Profile");

        pnl.eps = ["epsEncodingLabel", "epsEncoding", "epsEmbedColorProfile"];

        var it = pnl.epsEncoding.find(opts.epsEncoding);
        if (it) {
            pnl.epsEncoding.selection = it;
        }
        pnl.epsEmbedColorProfile.value = toBoolean(opts.epsEmbedColorProfile);

        x = xofs;
        y = yofs;
    }


    //=============================== PDF ===============================
    if (FileSaveOptionsTypes["pdf"]) {
        pnl.pdf = ["pdfEmbedColorProfile"];

        x = xofs;
        y = yofs;

        x = xofs;
        y += 30;
        pnl.pdfEmbedColorProfile = pnl.add('checkbox', [x, y, x + 155, y + 22],
            "Embed Color Profile");
        pnl.pdfEmbedColorProfile.value = toBoolean(opts.pdfEmbedColorProfile);

        x = xofs;
        y = yofs;
    }


    //=============================== PNG ===============================
    if (FileSaveOptionsTypes["png"]) {
        pnl.pngInterlaced = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Interlaced");

        pnl.png = ["pngInterlaced", "saveForWeb"];

        pnl.pngInterlaced.value = toBoolean(opts.pngInterlaced);

        pnl.saveForWeb.value = toBoolean(opts.saveForWeb);

        x = xofs;
        y = yofs;
    }


    //=============================== TGA ===============================
    if (FileSaveOptionsTypes["tga"]) {
        pnl.tgaAlphaChannels = pnl.add('checkbox', [x, y, x + 125, y + 22],
            "Alpha Channels");

        y += 30;

        pnl.tgaRLECompression = pnl.add('checkbox', [x, y, x + 145, y + 22],
            "RLE Compression");

        pnl.tga = ["tgaAlphaChannels", "tgaRLECompression"];

        pnl.tgaAlphaChannels.value = toBoolean(opts.tgaAlphaChannels);
        pnl.tgaRLECompression.value = toBoolean(opts.tgaRLECompression);

        x = xofs;
        y = yofs;
    }


    //=============================== TIFF ===============================
    if (FileSaveOptionsTypes["tif"]) {
        var tiffEncodingMenu = ["None", "LZW", "ZIP", "JPEG"];
        pnl.tiffEncodingLabel = pnl.add('statictext', [x, y + tOfs, x + 60, y + 22 + tOfs],
            'Encoding:');
        x += 65;
        pnl.tiffEncoding = pnl.add('dropdownlist', [x, y, x + 75, y + 22],
            tiffEncodingMenu);
        pnl.tiffEncoding.selection = pnl.tiffEncoding.find("None");

        pnl.tiffEncoding.find("None")._value = TIFFEncoding.NONE;
        pnl.tiffEncoding.find("LZW")._value = TIFFEncoding.TIFFLZW;
        pnl.tiffEncoding.find("ZIP")._value = TIFFEncoding.TIFFZIP;
        pnl.tiffEncoding.find("JPEG")._value = TIFFEncoding.JPEG;

        x += 90;

        var tiffByteOrderMenu = ["IBM", "MacOS"];
        pnl.tiffByteOrderLabel = pnl.add('statictext', [x, y + tOfs, x + 65, y + 22 + tOfs],
            'ByteOrder:');
        x += 70;
        pnl.tiffByteOrder = pnl.add('dropdownlist', [x, y, x + 85, y + 22],
            tiffByteOrderMenu);
        var bo = (isWindows() ? "IBM" : "MacOS");
        pnl.tiffByteOrder.selection = pnl.tiffByteOrder.find(bo);

        pnl.tiffByteOrder.find("IBM")._value = ByteOrder.IBM;
        pnl.tiffByteOrder.find("MacOS")._value = ByteOrder.MACOS;

        x = xofs;
        y += 30;
        pnl.tiffEmbedColorProfile = pnl.add('checkbox', [x, y, x + 155, y + 22],
            "Embed Color Profile");

        pnl.tif = ["tiffEncodingLabel", "tiffEncoding", "tiffByteOrderLabel",
            "tiffByteOrder", "tiffEmbedColorProfile", "flattenImage"];

        pnl.dng = [];

        var it = pnl.tiffEncoding.find(opts.tiffEncoding);
        if (it) {
            pnl.tiffEncoding.selection = it;
        }
        var it = pnl.tiffByteOrder.find(opts.tiffByteOrder);
        if (it) {
            pnl.tiffByteOrder.selection = it;
        }
        pnl.tiffEmbedColorProfile.value = toBoolean(opts.tiffEmbedColorProfile);

        pnl.flattenImage.value = toBoolean(opts.flattenImage);
    }

    pnl.fileType.onChange = function () {
        var pnl = this.parent;
        var ftsel = pnl.fileType.selection.index;
        var ft = FileSaveOptionsTypes[ftsel];

        for (var i = 0; i < FileSaveOptionsTypes.length; i++) {
            var fsType = FileSaveOptionsTypes[i];
            var parts = pnl[fsType.fileType];

            for (var j = 0; j < parts.length; j++) {
                var part = parts[j];
                pnl[part].visible = (fsType == ft);
            }
        }

        var fsType = ft.fileType;
        pnl.saveForWeb.visible = (pnl[fsType].contains("saveForWeb"));
        pnl.flattenImage.visible = (pnl[fsType].contains("flattenImage"));
        pnl._onChange();
    };

    pnl._onChange = function () {
        var self = this;
        if (self.onChange) {
            self.onChange();
        }
    };

    if (false) {
        y = yofs;
        x = 300;
        var btn = pnl.add('button', [x, y, x + 50, y + 22], "Test");
        btn.onClick = function () {
            try {
                var pnl = this.parent;
                var mgr = pnl.mgr;

                var opts = {};
                mgr.validateFileSavePanel(pnl, opts);
                alert(listProps(opts));
                alert(listProps(FileSaveOptions.convert(opts)));

            } catch (e) {
                var msg = psx.exceptionMessage(e);
                LogFile.write(msg);
                alert(msg);
            }
        };
    }

    pnl.fileType.onChange();

    pnl.getFileSaveType = function () {
        var pnl = this;
        var fstype = '';
        if (pnl.fileType.selection) {
            var fsSel = pnl.fileType.selection.index;
            var fs = FileSaveOptionsTypes[fsSel];
            fstype = fs.fileType;
        }
        return fstype;
    };

    pnl.updateSettings = function (ini) {
        var pnl = this;

        function _select(m, s, def) {
            var it = m.find(s.toString());
            if (!it && def != undefined) {
                it = m.items[def];
            }
            if (it) {
                m.selection = it;
            }
        }

        var opts = new FileSaveOptions(ini);
        var ftype = opts.fileType || opts.fileSaveType || "jpg";

        var ft = psx.getByProperty(FileSaveOptionsTypes,
            "fileType",
            ftype);
        pnl.fileType.selection = pnl.fileType.find(ft.menu);

        if (FileSaveOptionsTypes["bmp"]) {
            pnl.bmpAlphaChannels.value = toBoolean(opts.bmpAlphaChannels);
            _select(pnl.bmpDepth, opts.bmpDepth.toString(), 0);
            pnl.bmpRLECompression.value = toBoolean(opts.bmpRLECompression);
        }

        if (FileSaveOptionsTypes["gif"]) {
            pnl.gifTransparency.value = toBoolean(opts.gifTransparency);
            pnl.gifInterlaced.value = toBoolean(opts.gifInterlaced);
            pnl.gifColors.text = toNumber(opts.gifColors || 256);
            pnl.saveForWeb.value = toBoolean(opts.saveForWeb);
        }

        if (FileSaveOptionsTypes["jpg"]) {
            _select(pnl.jpgQuality, opts.jpgQuality.toString(), 0);
            pnl.jpgEmbedColorProfile.value = toBoolean(opts.jpgEmbedColorProfile);
            _select(pnl.jpgFormat, opts.jpgFormat, 0);
            pnl.jpgConvertToSRGB.value = toBoolean(opts.jpgConvertToSRGB);
            pnl.saveForWeb.value = toBoolean(opts.saveForWeb);
        }

        if (FileSaveOptionsTypes["psd"]) {
            pnl.psdAlphaChannels.value = toBoolean(opts.psdAlphaChannels);
            pnl.psdEmbedColorProfile.value = toBoolean(opts.psdEmbedColorProfile);
            pnl.psdLayers.value = toBoolean(opts.psdLayers);
            pnl.psdMaximizeCompatibility.value =
                toBoolean(opts.psdMaximizeCompatibility);
            pnl.flattenImage.value = toBoolean(opts.flattenImage);
        }

        if (FileSaveOptionsTypes["eps"]) {
            _select(pnl.epsEncoding, opts.epsEncoding, 0);
            pnl.epsEmbedColorProfile.value = toBoolean(opts.epsEmbedColorProfile);
        }

        if (FileSaveOptionsTypes["pdf"]) {
            pnl.pdfEmbedColorProfile.value = toBoolean(opts.pdfEmbedColorProfile);
        }

        if (FileSaveOptionsTypes["png"]) {
            pnl.pngInterlaced.value = toBoolean(opts.pngInterlaced);
            pnl.saveForWeb.value = toBoolean(opts.saveForWeb);
        }

        if (FileSaveOptionsTypes["tga"]) {
            pnl.tgaAlphaChannels.value = toBoolean(opts.tgaAlphaChannels);
            pnl.tgaRLECompression.value = toBoolean(opts.tgaRLECompression);
        }

        if (FileSaveOptionsTypes["tif"]) {
            _select(pnl.tiffEncoding, opts.tiffEncoding, 0);
            _select(pnl.tiffByteOrder, opts.tiffByteOrder, 0);
            pnl.tiffEmbedColorProfile.value = toBoolean(opts.tiffEmbedColorProfile);
            pnl.flattenImage.value = toBoolean(opts.flattenImage);
        }

        pnl.fileType.onChange();
    }

    return pnl;
};
FileSaveOptions.validateFileSavePanel = function (pnl, opts) {
    var win = pnl.window;

    // XXX This function needs to remove any prior file save
    // options and only set the ones needed for the
    // selected file type

    var fsOpts = new FileSaveOptions();
    for (var idx in fsOpts) {
        if (idx in opts) {
            delete opts[idx];
        }
    }

    var fsSel = pnl.fileType.selection.index;
    var fs = FileSaveOptionsTypes[fsSel];

    opts.fileSaveType = fs.fileType;
    opts._saveDocumentType = fs.saveType;

    if (!fs.optionsType) {
        opts._saveOpts = undefined;
        return;
    }

    var saveOpts = new fs.optionsType();

    switch (fs.fileType) {
        case "bmp": {
            saveOpts.rleCompression = pnl.bmpRLECompression.value;
            saveOpts.depth = pnl.bmpDepth.selection._value;
            saveOpts.alphaChannels = pnl.bmpAlphaChannels.value;

            opts.bmpRLECompression = pnl.bmpRLECompression.value;
            opts.bmpDepth = Number(pnl.bmpDepth.selection.text);
            opts.bmpAlphaChannels = pnl.bmpAlphaChannels.value;
            break;
        }
        case "gif": {
            saveOpts.transparency = pnl.gifTransparency.value;
            saveOpts.interlaced = pnl.gifInterlaced.value;
            var colors = toNumber(pnl.gifColors.text || 256);
            if (colors < 2) { colors = 2; }
            if (colors > 256) { colors = 256; }
            saveOpts.colors = colors;
            saveOpts._saveForWeb = pnl.saveForWeb.value;

            opts.gifTransparency = pnl.gifTransparency.value;
            opts.gifInterlaced = pnl.gifInterlaced.value;
            opts.gifColors = colors;
            opts.saveForWeb = pnl.saveForWeb.value;
            break;
        }
        case "jpg": {
            saveOpts.quality = Number(pnl.jpgQuality.selection.text);
            saveOpts.embedColorProfile = pnl.jpgEmbedColorProfile.value;
            saveOpts.formatOptions = pnl.jpgFormat.selection._value;
            saveOpts._convertToSRGB = pnl.jpgConvertToSRGB.value;
            saveOpts._saveForWeb = pnl.saveForWeb.value;

            opts.jpgQuality = Number(pnl.jpgQuality.selection.text);
            opts.jpgEmbedColorProfile = pnl.jpgEmbedColorProfile.value;
            opts.jpgFormat = pnl.jpgFormat.selection.text;
            opts.jpgConvertToSRGB = pnl.jpgConvertToSRGB.value;
            opts.saveForWeb = pnl.saveForWeb.value;
            break;
        }
        case "psd": {
            saveOpts.alphaChannels = pnl.psdAlphaChannels.value;
            saveOpts.embedColorProfile = pnl.psdEmbedColorProfile.value;
            saveOpts.layers = pnl.psdLayers.value;
            saveOpts.maximizeCompatibility = pnl.psdMaximizeCompatibility.value;

            opts.psdAlphaChannels = pnl.psdAlphaChannels.value;
            opts.psdEmbedColorProfile = pnl.psdEmbedColorProfile.value;
            opts.psdLayers = pnl.psdLayers.value;
            opts.psdMaximizeCompatibility = pnl.psdMaximizeCompatibility.value;
            opts.flattenImage = pnl.flattenImage.value;
            break;
        }
        case "eps": {
            saveOpts.encoding = pnl.epsEncoding.selection._value;
            saveOpts.embedColorProfile = pnl.epsEmbedColorProfile.value;

            opts.epsEncoding = pnl.epsEncoding.selection.text;
            opts.epsEmbedColorProfile = pnl.epsEmbedColorProfile.value;
            break;
        }
        case "pdf": {
            saveOpts.embedColorProfile = pnl.pdfEmbedColorProfile.value;

            opts.pdfEmbedColorProfile = pnl.pdfEmbedColorProfile.value;
            break;
        }
        case "png": {
            saveOpts.interlaced = pnl.pngInterlaced.value;
            saveOpts._saveForWeb = pnl.saveForWeb.value;

            opts.pngInterlaced = pnl.pngInterlaced.value;
            opts.saveForWeb = pnl.saveForWeb.value;
            break;
        }
        case "tga": {
            saveOpts.alphaChannels = pnl.tgaAlphaChannels.value;
            saveOpts.rleCompression = pnl.tgaRLECompression.value;

            opts.tgaAlphaChannels = pnl.tgaAlphaChannels.value;
            opts.tgaRLECompression = pnl.tgaRLECompression.value;
            break;
        }
        case "tif": {
            saveOpts.byteOrder = pnl.tiffByteOrder.selection._value;
            saveOpts.imageCompression = pnl.tiffEncoding.selection._value;
            saveOpts.embedColorProfile = pnl.tiffEmbedColorProfile.value;

            opts.tiffByteOrder = pnl.tiffByteOrder.selection.text;
            opts.tiffEncoding = pnl.tiffEncoding.selection.text;
            opts.tiffEmbedColorProfile = pnl.tiffEmbedColorProfile.value;
            opts.flattenImage = pnl.flattenImage.value;
            break;
        }
        default:
            Error.runtimeError(9001, "Internal Error: Unknown file type: " +
                fs.fileType);
    }

    opts._saveOpts = saveOpts;

    return;
};

//============================= FileNaming ====================================
//
// Function: _getFontArray
// Description: 
// Input:  <none> 
// Return: an array of font info objects created by _getFontTable
//
//
FileNamingOptions = function (obj, prefix) {
    var self = this;

    self.fileNaming = [];      // array of FileNamingType and/or String
    self.startingSerial = 1;
    self.windowsCompatible = isWindows();
    self.macintoshCompatible = isMac();
    self.unixCompatible = true;

    if (obj) {
        if (prefix == undefined) {
            prefix = '';
        }
        var props = FileNamingOptions.props;
        for (var i = 0; i < props.length; i++) {
            var name = props[i];
            var oname = prefix + name;
            if (oname in obj) {
                self[name] = obj[oname];
            }
        }

        if (self.fileNaming.constructor == String) {
            self.fileNaming = self.fileNaming.split(',');

            // remove "'s from around custom text
        }
    }
};
FileNamingOptions.prototype.typename = FileNamingOptions;
FileNamingOptions.props = ["fileNaming", "startingSerial", "windowsCompatible",
    "macintoshCompatible", "unixCompatible"];

FileNamingOptions.prototype.format = function (file, cdate) {
    var self = this;
    var str = '';

    file = psx.convertFptr(file);

    if (!cdate) {
        cdate = file.created || new Date();
    }

    var fname = file.strf("%f");
    var ext = file.strf("%e");

    var parts = self.fileNaming;

    if (parts.constructor == String) {
        parts = parts.split(',');
    }

    var serial = self.startingSerial;
    var aCode = 'a'.charCodeAt(0);
    var ACode = 'A'.charCodeAt(0);
    var hasSerial = false;

    for (var i = 0; i < parts.length; i++) {
        var p = parts[i];
        var fnel = FileNamingElements.getByName(p);

        if (!fnel) {
            str += p;
            continue;
        }

        var s = '';
        switch (fnel.type) {
            case FileNamingType.DOCUMENTNAMEMIXED: s = fname; break;
            case FileNamingType.DOCUMENTNAMELOWER: s = fname.toLowerCase(); break;
            case FileNamingType.DOCUMENTNAMEUPPER: s = fname.toUpperCase(); break;
            case FileNamingType.SERIALNUMBER1:
                s = "%d".sprintf(serial);
                hasSerial = true;
                break;
            case FileNamingType.SERIALNUMBER2:
                s = "%02d".sprintf(serial);
                hasSerial = true;
                break;
            case FileNamingType.SERIALNUMBER3:
                s = "%03d".sprintf(serial);
                hasSerial = true;
                break;
            case FileNamingType.SERIALNUMBER4:
                s = "%04d".sprintf(serial);
                hasSerial = true;
                break;
            case FileNamingElement.SERIALNUMBER5:
                s = "%05d".sprintf(serial);
                hasSerial = true;
                break;
            case FileNamingType.EXTENSIONLOWER: s = '.' + ext.toLowerCase(); break;
            case FileNamingType.EXTENSIONUPPER: s = '.' + ext.toUpperCase(); break;
            case FileNamingType.SERIALLETTERLOWER:
                s = FileNamingOptions.nextAlphaIndex(aCode, serial);
                hasSerial = true;
                break;
            case FileNamingType.SERIALLETTERUPPER:
                s = FileNamingOptions.nextAlphaIndex(ACode, serial);
                hasSerial = true;
                break;
        }

        if (s) {
            str += s;
            continue;
        }

        var fmt = '';
        switch (fnel.type) {
            case FileNamingType.MMDDYY: fmt = "%m%d%y"; break;
            case FileNamingType.MMDD: fmt = "%m%d"; break;
            case FileNamingType.YYYYMMDD: fmt = "%Y%m%d"; break;
            case FileNamingType.YYMMDD: fmt = "%y%m%d"; break;
            case FileNamingType.YYDDMM: fmt = "%y%d%m"; break;
            case FileNamingType.DDMMYY: fmt = "%d%m%y"; break;
            case FileNamingType.DDMM: fmt = "%d%m"; break;
        }

        if (fmt) {
            str += cdate.strftime(fmt);
            continue;
        }
    }

    if (hasSerial) {
        serial++;
    }

    self._serial = serial;

    return str;
};

FileNamingOptions.nextAlphaIndex = function (base, idx) {
    var str = '';

    while (idx > 0) {
        idx--;
        var m = idx % 26;
        var idx = Math.floor(idx / 26);

        str = String.fromCharCode(m + base) + str;
    }

    return str;
};

FileNamingOptions.prototype.copyTo = function (opts, prefix) {
    var self = this;
    var props = FileNamingOptions.props;

    for (var i = 0; i < props.length; i++) {
        var name = props[i];
        var oname = prefix + name;
        opts[oname] = self[name];
        if (name == 'fileNaming' && self[name] instanceof Array) {
            opts[oname] = self[name].join(',');
        } else {
            opts[oname] = self[name];
        }
    }
};


// this array is folder into FileNamingElement
FileNamingOptions._examples =
    ["",
        "Document",
        "document",
        "DOCUMENT",
        "1",
        "01",
        "001",
        "0001",
        "a",
        "A",
        "103107",
        "1031",
        "20071031",
        "071031",
        "073110",
        "311007",
        "3110",
        ".Psd",
        ".psd",
        ".PSD"
    ];

FileNamingOptions.prototype.getExample = function () {
    var self = this;
    var str = '';
    return str;
};

FileNamingElement = function (name, menu, type, sm, example) {
    var self = this;
    self.name = name;
    self.menu = menu;
    self.type = type;
    self.smallMenu = sm;
    self.example = (example || '');
};
FileNamingElement.prototype.typename = FileNamingElement;

FileNamingElements = [];
FileNamingElements._add = function (name, menu, type, sm, ex) {
    FileNamingElements.push(new FileNamingElement(name, menu, type, sm, ex));
}

FileNamingElement.NONE = "(None)";

FileNamingElement.SERIALNUMBER5 = {
    toString: function () { return "FileNamingElement.SERIALNUMBER5"; }
};

FileNamingElements._init = function () {

    FileNamingElements._add("", "", "", "", ""); // Same as (None)

    try {
        FileNamingType;
    } catch (e) {
        return;
    }

    // the names here correspond to the sTID symbols used when making
    // a Batch request via the ActionManager interface. Except for "Name",
    // which should be "Nm  ".
    // the names should be the values used when serializing to and from
    // an INI file.
    // A FileNamingOptions object needs to be defined.
    FileNamingElements._add("Name", ZStrs.DocumentName,
        FileNamingType.DOCUMENTNAMEMIXED,
        "Name", "Document");
    FileNamingElements._add("lowerCase", ZStrs.LCDocumentName,
        FileNamingType.DOCUMENTNAMELOWER,
        "name", "document");
    FileNamingElements._add("upperCase", ZStrs.UCDocumentName,
        FileNamingType.DOCUMENTNAMEUPPER,
        "NAME", "DOCUMENT");
    FileNamingElements._add("oneDigit", ZStrs.FN1Digit,
        FileNamingType.SERIALNUMBER1,
        "Serial #", "1");
    FileNamingElements._add("twoDigit", ZStrs.FN2Digit,
        FileNamingType.SERIALNUMBER2,
        "Serial ##", "01");
    FileNamingElements._add("threeDigit", ZStrs.FN3Digit,
        FileNamingType.SERIALNUMBER3,
        "Serial ###", "001");
    FileNamingElements._add("fourDigit", ZStrs.FN4Digit,
        FileNamingType.SERIALNUMBER4,
        "Serial ####", "0001");
    FileNamingElements._add("fiveDigit", ZStrs.FN5Digit,
        FileNamingElement.SERIALNUMBER5,
        "Serial #####", "00001");
    FileNamingElements._add("lowerCaseSerial", ZStrs.LCSerial,
        FileNamingType.SERIALLETTERLOWER,
        "Serial a", "a");
    FileNamingElements._add("upperCaseSerial", ZStrs.UCSerial,
        FileNamingType.SERIALLETTERUPPER,
        "Serial A", "A");
    FileNamingElements._add("mmddyy", ZStrs.Date_mmddyy,
        FileNamingType.MMDDYY,
        "mmddyy", "103107");
    FileNamingElements._add("mmdd", ZStrs.Date_mmdd,
        FileNamingType.MMDD,
        "mmdd", "1031");
    FileNamingElements._add("yyyymmdd", ZStrs.Date_yyyymmdd,
        FileNamingType.YYYYMMDD,
        "yyyymmdd", "20071031");
    FileNamingElements._add("yymmdd", ZStrs.Date_yymmdd,
        FileNamingType.YYMMDD,
        "yymmdd", "071031");
    FileNamingElements._add("yyddmm", ZStrs.Date_yyddmm,
        FileNamingType.YYDDMM,
        "yyddmm", "073110");
    FileNamingElements._add("ddmmyy", ZStrs.Date_ddmmyy,
        FileNamingType.DDMMYY,
        "ddmmyy", "311007");
    FileNamingElements._add("ddmm", ZStrs.Date_ddmm,
        FileNamingType.DDMM,
        "ddmm", "3110");
    FileNamingElements._add("lowerCaseExtension", ZStrs.LCExtension,
        FileNamingType.EXTENSIONLOWER,
        "ext", ".psd");
    FileNamingElements._add("upperCaseExtension", ZStrs.UCExtension,
        FileNamingType.EXTENSIONUPPER,
        "EXT", ".PSD");
};
FileNamingElements._init();
FileNamingElements.getByName = function (name) {
    return psx.getByName(FileNamingElements, name);
};

FileNamingOptions.CUSTOM_TEXT_CREATE = "Create";
FileNamingOptions.CUSTOM_TEXT_DELETE = "Delete";
FileNamingOptions.CUSTOM_TEXT_EDIT = "Edit";

FileNamingOptions.createFileNamingPanel = function (pnl, ini,
    prefix,
    useSerial,
    useCompatibility,
    columns) {
    var win = pnl.window;
    if (useSerial == undefined) {
        useSerial = false;
    }
    if (useCompatibility == undefined) {
        useCompatibility = false;
    }
    if (columns == undefined) {
        columns = 3;
    } else {
        if (columns != 2 && columns != 3) {
            Error.runtimeError(9001, "Internal Error: Bad column spec for " +
                "FileNaming panel");
        }
    }

    pnl.fnmenuElements = [];
    for (var i = 0; i < FileNamingElements.length; i++) {
        var fnel = FileNamingElements[i];
        pnl.fnmenuElements.push(fnel.menu);
    }
    var extrasMenuEls = [
        "-",
        ZStrs.CreateCustomText,
        ZStrs.EditCustomText,
        //     ZStrs.DeleteCustomText,
        "-",
        FileNamingElement.NONE,
    ];
    for (var i = 0; i < extrasMenuEls.length; i++) {
        pnl.fnmenuElements.push(extrasMenuEls[i]);
    }

    pnl.win = win;
    if (prefix == undefined) {
        prefix = '';
    }
    pnl.prefix = prefix;

    var w = pnl.bounds[2] - pnl.bounds[0];
    var xofs = 0;
    var y = 0;

    if (pnl.type == 'panel') {
        xofs += 5;
        y += 10;
    }
    pnl.text = ZStrs.FileNaming;

    var tOfs = 3;

    if (columns == 2) {
        var menuW = (w - 50) / 2;

    } else {
        var menuW = (w - 65) / 3;
    }

    var opts = new FileNamingOptions(ini, pnl.prefix);

    x = xofs;

    pnl.exampleLabel = pnl.add('statictext', [x, y + tOfs, x + 70, y + 22 + tOfs],
        ZStrs.ExampleLabel);
    x += 70;
    pnl.example = pnl.add('statictext', [x, y + tOfs, x + 250, y + 22 + tOfs], '');
    y += 30;
    x = xofs;

    pnl.menus = [];

    pnl.menus[0] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    x += menuW + 5;
    pnl.add('statictext', [x, y + tOfs, x + 10, y + 22 + tOfs], '+');

    x += 15;

    pnl.menus[1] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    x += menuW + 5;
    pnl.add('statictext', [x, y + tOfs, x + 10, y + 22 + tOfs], '+');

    if (columns == 2) {
        y += 30;
        x = xofs;
    } else {
        x += 15;
    }

    pnl.menus[2] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    x += menuW + 5;
    pnl.add('statictext', [x, y + tOfs, x + 10, y + 22 + tOfs], '+');

    if (columns == 3) {
        y += 30;
        x = xofs;

    } else {
        x += 15;
    }

    pnl.menus[3] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    x += menuW + 5;
    pnl.add('statictext', [x, y + tOfs, x + 10, y + 22 + tOfs], '+');

    if (columns == 2) {
        y += 30;
        x = xofs;

    } else {
        x += 15;
    }

    pnl.menus[4] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    x += menuW + 5;
    pnl.add('statictext', [x, y + tOfs, x + 10, y + 22 + tOfs], '+');

    x += 15;

    pnl.menus[5] = pnl.add('dropdownlist', [x, y, x + menuW, y + 22],
        pnl.fnmenuElements);
    y += 30;
    x = xofs;

    pnl.addMenuElement = function (text) {
        var pnl = this;
        for (var i = 0; i < 6; i++) {
            var vmenu = pnl.menus[i];
            vmenu.add('item', text);
        }
    }

    pnl.addCustomMenuElement = function (text) {
        var pnl = this;
        if (text == '-') {
            text = '- ';
        }
        for (var i = 0; i < 6; i++) {
            var vmenu = pnl.menus[i];
            var it = menu.find(text);
            if (it == undefined) {
                vmenu.add('item', text);
            }
        }
    }

    pnl.useSerial = useSerial;
    if (useSerial) {
        pnl.startingSerialLbl = pnl.add('statictext', [x, y + tOfs, x + 80, y + 22 + tOfs],
            ZStrs.StartingSerialNumber);
        x += 90;
        pnl.startingSerial = pnl.add('edittext', [x, y, x + 50, y + 22],
            opts.startingSerial);
        y += 30;
        x = xofs;
        pnl.startingSerial.onChanging = psx.numberKeystrokeFilter;
        pnl.startingSerial.onChange = function () {
            var pnl = this.parent;
            pnl.onChange();
        }
    }

    pnl.useCompatibility = useCompatibility;
    if (useCompatibility) {
        pnl.add('statictext', [x, y + tOfs, x + 80, y + 22 + tOfs], ZStrs.CompatibilityPrompt);
        x += 90;
        pnl.compatWindows = pnl.add('checkbox', [x, y, x + 70, y + 22], ZStrs.Windows);
        x += 80;
        pnl.compatMac = pnl.add('checkbox', [x, y, x + 70, y + 22], ZStrs.MacOS);
        x += 80;
        pnl.compatUnix = pnl.add('checkbox', [x, y, x + 70, y + 22], ZStrs.Unix);

        pnl.compatWindows.value = opts.windowsCompatible;
        pnl.compatMac.value = opts.macintoshCompatible;
        pnl.compatUnix.value = opts.unixCompatible;
    }

    function menuOnChange() {
        var pnl = this.parent;
        var win = pnl.window;
        if (pnl.processing) {
            return;
        }
        pnl.processing = true;
        try {
            var menu = this;
            if (!menu.selection) {
                return;
            }

            var currentSelection = menu.selection.index;
            var lastSelection = menu.lastMenuSelection;

            menu.lastMenuSelection = menu.selection.index;

            var lastWasCustomText = (lastSelection >= pnl.fnmenuElements.length);

            var sel = menu.selection.text;
            if (sel == FileNamingElement.NONE) {
                menu.selection = menu.items[0];
                sel = menu.selection.text;
            }

            if (sel == ZStrs.CreateCustomText ||
                (sel == ZStrs.EditCustomText && !lastWasCustomText)) {
                var text = FileNamingOptions.createCustomTextDialog(win,
                    ZStrs.CreateCustomText,
                    "new");
                if (text) {
                    if (text == '-') {
                        text = '- ';
                    }
                    if (!menu.find(text)) {
                        pnl.addMenuElement(text);
                    }

                    var it = menu.find(text);
                    menu.selection = it;

                } else {
                    if (lastSelection >= 0) {
                        menu.selection = menu.items[lastSelection];
                    } else {
                        menu.selection = menu.items[0];
                    }
                }

                if (pnl.notifyCustomText) {
                    pnl.notifyCustomText(FileNamingOptions.CUSTOM_TEXT_CREATE, text);
                }

            } else if (lastWasCustomText) {
                if (sel == ZStrs.EditCustomText) {
                    var lastText = menu.items[lastSelection].text;
                    if (lastText == '- ') {
                        lastText = '-'
                    }
                    var text = FileNamingOptions.createCustomTextDialog(win,
                        ZStrs.EditCustomText,
                        "edit",
                        lastText);
                    if (text) {
                        if (text == '-') {
                            text = '- ';
                        }
                        for (var i = 0; i < 6; i++) {
                            var vmenu = pnl.menus[i];
                            vmenu.items[lastSelection].text = text;
                        }

                        var it = menu.find(text);
                        menu.selection = it;

                        if (pnl.notifyCustomText) {
                            if (lastText == '-') {
                                lastText = '- ';
                            }

                            pnl.notifyCustomText(FileNamingOptions.CUSTOM_TEXT_EDIT, text,
                                lastText);
                        }

                    } else {
                        if (lastSelection >= 0) {
                            menu.selection = menu.items[lastSelection];
                        } else {
                            menu.selection = menu.items[0];
                        }
                    }

                } else if (sel == ZStrs.DeleteCustomText) {
                    var lastText = menu.items[lastSelection].text;
                    if (confirm(ZStrs.DeleteCustomTextPrompt.sprintf(lastText))) {
                        for (var i = 0; i < 6; i++) {
                            var vmenu = pnl.menus[i];
                            vmenu.remove(lastSelection);
                        }
                        menu.selection = menu.items[0];

                    } else {
                        menu.selection = menu.items[lastSelection];
                    }

                    if (pnl.notifyCustomText) {
                        pnl.notifyCustomText(FileNamingOptions.CUSTOM_TEXT_DELETE, lastText);
                    }

                } else {
                    //alert("Internal error, Custom Text request");
                }

            } else {
                if (lastSelection >= 0 && (sel == ZStrs.EditCustomText ||
                    sel == ZStrs.DeleteCustomText)) {
                    menu.selection = menu.items[lastSelection];
                }
            }

            menu.lastMenuSelection = menu.selection.index;

            var example = '';
            var format = [];

            for (var i = 0; i < 6; i++) {
                var vmenu = pnl.menus[i];
                if (vmenu.selection) {
                    var fmt = '';
                    var text = vmenu.selection.text;
                    var fne = psx.getByProperty(FileNamingElements, "menu", text);
                    if (fne) {
                        text = fne.example;
                        fmt = fne.name;
                    } else {
                        fmt = text;
                    }

                    if (text) {
                        if (text == '- ') {
                            text = '-';
                        }
                        example += text;
                    }

                    if (fmt) {
                        if (fmt == '- ') {
                            fmt = '-';
                        }
                        format.push(fmt);
                    }
                }
            }
            if (pnl.example) {
                pnl.example.text = example;
            }
            format = format.join(",");
            var win = pnl.window;
            if (win.mgr.updateNamingFormat) {
                win.mgr.updateNamingFormat(format, example);
            }

        } finally {
            pnl.processing = false;
        }

        if (pnl.onChange) {
            pnl.onChange();
        }
    }

    // default all slots to ''
    for (var i = 0; i < 6; i++) {
        var menu = pnl.menus[i];
        menu.selection = menu.items[0];
        menu.lastMenuSelection = 0;
    }

    for (var i = 0; i < 6; i++) {
        var name = opts.fileNaming[i];
        if (name) {
            var fne = FileNamingElements.getByName(name);
            var it;

            if (!fne) {
                if (name == '- ') {
                    name = '-';
                }
                it = pnl.menus[i].find(name);
                if (!it) {
                    pnl.addMenuElement(name);
                    it = pnl.menus[i].find(name);
                }
            } else {
                it = pnl.menus[i].find(fne.menu);
            }
            pnl.menus[i].selection = it;
        }
    }

    //   pnl.menus[0].selection = pnl.menus[0].find("document name");
    //   pnl.menus[0].lastMenuSelection = pnl.menus[0].selection.index;
    //   pnl.menus[1].selection = pnl.menus[1].find("extension");
    //   pnl.menus[1].lastMenuSelection = pnl.menus[1].selection.index;

    for (var i = 0; i < 6; i++) {
        var menu = pnl.menus[i];
        menu.onChange = menuOnChange;
    }

    pnl.getFileNamingOptions = function (ini) {
        var pnl = this;
        var fileNaming = [];

        for (var i = 0; i < 6; i++) {
            var menu = pnl.menus[i];

            if (menu.selection) {
                var idx = menu.selection.index;

                if (idx) {
                    // [0] is the "" item so we ignore it
                    var fnel = FileNamingElements[idx];
                    if (fnel) {
                        fileNaming.push(fnel.name);

                    } else {
                        // its a custom naming option
                        var txt = menu.selection.text;
                        if (txt == '- ') {
                            txt = '-';
                        }

                        // txt = '"' + text + '"';
                        fileNaming.push(txt);
                    }
                }
            }
        }

        var prefix = pnl.prefix;
        var opts = new FileNamingOptions(ini, prefix);
        opts.fileNaming = fileNaming;

        if (pnl.startingSerial) {
            opts.startingSerial = Number(pnl.startingSerial.text);
        }
        if (pnl.compatWindows) {
            opts.windowsCompatible = pnl.compatWindows.value;
        }
        if (pnl.compatMac) {
            opts.macintoshCompatible = pnl.compatMac.value;
        }
        if (pnl.compatUnix) {
            opts.unixCompatible = pnl.compatUnix.value;
        }
        return opts;
    }
    pnl.getFilenamingOptions = pnl.getFileNamingOptions;

    pnl.updateSettings = function (ini) {
        var pnl = this;

        var opts = new FileNamingOptions(ini, pnl.prefix);

        if (pnl.useSerial) {
            pnl.startingSerial.text = opts.startingSerial;
        }

        if (pnl.useCompatibility) {
            pnl.compatWindows.value = opts.windowsCompatible;
            pnl.compatMac.value = opts.macintoshCompatible;
            pnl.compatUnix.value = opts.unixCompatible;
        }

        // default all slots to ''
        for (var i = 0; i < 6; i++) {
            var menu = pnl.menus[i];
            menu.selection = menu.items[0];
            menu.lastMenuSelection = 0;
        }

        for (var i = 0; i < 6; i++) {
            var name = opts.fileNaming[i];
            if (name) {
                var fne = FileNamingElements.getByName(name);
                var it;

                if (!fne) {
                    if (name == '-') {
                        name = '- ';
                    }
                    it = pnl.menus[i].find(name);
                    if (!it) {
                        pnl.addMenuElement(name);
                        it = pnl.menus[i].find(name);
                    }
                } else {
                    it = pnl.menus[i].find(fne.menu);
                }
                pnl.menus[i].selection = it;
            }
        }

        for (var i = 0; i < 6; i++) {
            var menu = pnl.menus[i];
            menu.onChange = menuOnChange;
        }

        pnl.menus[0].onChange();

        if (pnl.onChange) {
            pnl.onChange();
        }
    }

    pnl.updateCustomText = function (event, text, oldText) {
        var pnl = this;

        if (!event || !text) {
            return;
        }

        if (event == FileNamingOptions.CUSTOM_TEXT_CREATE) {
            if (!pnl.menus[0].find(text)) {
                pnl.addMenuElement(text);
            }

        } else if (event == FileNamingOptions.CUSTOM_TEXT_DELETE) {
            var it = pnl.menus[0].find(text);
            if (it) {
                var idx = it.index;
                for (var i = 0; i < 6; i++) {
                    var vmenu = pnl.menus[i];
                    vmenu.remove(idx);
                }
            }

        } else if (event == FileNamingOptions.CUSTOM_TEXT_EDIT) {
            if (oldText) {
                var idx = -1;
                var it = pnl.menus[0].find(oldText);
                if (it) {
                    idx = it.index;

                    for (var i = 0; i < 6; i++) {
                        var vmenu = pnl.menus[i];
                        vmenu.items[idx].text = text;
                    }
                } else {
                    pnl.updateCustomText(FileNamingOptions.CUSTOM_TEXT_CREATE,
                        text);
                }
            } else {
                pnl.updateCustomText(FileNamingOptions.CUSTOM_TEXT_CREATE,
                    text);
            }
        }

        if (pnl.onChange) {
            pnl.onChange();
        }
    }

    pnl.menus[0].onChange();

    if (pnl.onChange) {
        pnl.onChange();
    }

    return pnl;
};
FileNamingOptions.createCustomTextDialog = function (win, title, mode, init) {
    var rect = {
        x: 200,
        y: 200,
        w: 350,
        h: 150
    };

    function rectToBounds(r) {
        return [r.x, r.y, r.x + r.w, r.y + r.h];
    };

    var cwin = new Window('dialog', title || ZStrs.CustomTextEditor,
        rectToBounds(rect));

    cwin.text = title || ZStrs.CustomTextEditor;
    if (win) {
        cwin.center(win);
    }

    var xofs = 10;
    var y = 10;
    var x = xofs;

    var tOfs = 3;

    cwin.add('statictext', [x, y + tOfs, x + 300, y + 22 + tOfs], ZStrs.CustomTextPrompt);

    y += 30;
    cwin.customText = cwin.add('edittext', [x, y, x + 330, y + 22]);

    cwin.customText.onChanging = function () {
        cwin = this.parent;
        var text = cwin.customText.text;

        if (cwin.initText) {
            cwin.saveBtn.enabled = (text.length > 0) && (text != cwin.initText);
        } else {
            cwin.saveBtn.enabled = (text.length > 0);
        }
    }

    if (init) {
        cwin.customText.text = init;
        cwin.initText = init;
    }

    y += 50;
    x += 100;
    cwin.saveBtn = cwin.add('button', [x, y, x + 70, y + 22], ZStrs.Save);
    cwin.saveBtn.enabled = false;

    x += 100;
    cwin.cancelBtn = cwin.add('button', [x, y, x + 70, y + 22], ZStrs.Cancel);

    cwin.defaultElement = cwin.saveBtn;

    cwin.customText.active = true;

    cwin.onShow = function () {
        this.customText.active = true;
    }

    var res = cwin.show();
    return (res == 1) ? cwin.customText.text : undefined;
};

FileNamingOptions.validateFileNamingPanel = function (pnl, opts) {
    var self = this;
    var win = pnl.window;
    var fopts = pnl.getFileNamingOptions(opts);

    if (fopts.fileNaming.length == 0) {
        return self.errorPrompt("You must specify a name for the files.");
    }

    fopts.copyTo(opts, pnl.prefix);

    return opts;
};


//======================== Font Panel =================================
//
// Function: createFontPanel
// Description: Creates a font selector panel
// Input: pnl - the panel that will be populated
//        ini - an object that contains initial values (not used)
//        label  - the lable for the panel (opt)
//        lwidth - the width to use for the lable in the UI
// Return: the panel
//
psxui.createFontPanel = function (pnl, ini, label, lwidth) {
    var win = pnl.window;

    pnl.win = win;

    var w = pnl.bounds[2] - pnl.bounds[0];
    var xofs = 0;
    var y = 0;

    if (pnl.type == 'panel') {
        xofs += 5;
        y += 5;
    }

    var tOfs = 0;
    var x = xofs;

    if (label == undefined) {
        label = ZStrs.FontLabel;
        lwidth = pnl.graphics.measureString(label)[0] + 5;
    }

    if (label != '') {
        pnl.label = pnl.add('statictext', [x, y + tOfs, x + lwidth, y + 22 + tOfs], label);
        pnl.label.helpTip = ZStrs.FontTip;
        x += lwidth;
    }
    pnl.family = pnl.add('dropdownlist', [x, y, x + 200, y + 22]);
    pnl.family.helpTip = ZStrs.FontTip;
    x += 210;
    pnl.style = pnl.add('dropdownlist', [x, y, x + 170, y + 22]);
    pnl.style.helpTip = ZStrs.FontStyleTip;
    x += 180;
    pnl.fontSize = pnl.add('editnumber', [x, y, x + 30, y + 22], 12, ContactSheetUI.MIN_FONTSIZE, ContactSheetUI.MAX_FONTSIZE, { numberFormat: "integer" });
    pnl.fontSize.helpTip = ZStrs.FontSizeTip;
    x += 34;
    pnl.sizeLabel = pnl.add('statictext', [x, y + tOfs, x + 15, y + 22 + tOfs],
        ZStrs.UnitsPT);

    var lbl = pnl.sizeLabel;
    lbl.bounds.width = pnl.graphics.measureString(ZStrs.UnitsPT)[0] + 3;

    // make adjustments if panel is not wide enough to display all of the
    // controls steal space from the family and style dropdown menus
    var pw = pnl.bounds.width;
    var slMax = pnl.sizeLabel.bounds.right;
    var diff = slMax - pw;
    if (diff > 0) {
        diff += 6; // for padding on the right side
        var delta = Math.ceil(diff / 2);
        pnl.family.bounds.width -= delta;
        pnl.style.bounds.left -= delta;
        delta *= 2;
        pnl.style.bounds.width -= delta;
        pnl.fontSize.bounds.left -= delta;
        pnl.fontSize.bounds.right -= delta;
        pnl.sizeLabel.bounds.left -= delta;
        pnl.sizeLabel.bounds.right -= delta;
    }

    pnl.fontTable = psxui._getFontTable();
    var names = [];
    for (var idx in pnl.fontTable) {
        names.push(idx);
    }
    // names.sort();
    for (var i = 0; i < names.length; i++) {
        pnl.family.add('item', names[i]);
    }

    pnl.family.onChange = function () {
        var pnl = this.parent;
        var sel = pnl.family.selection.text;
        var family = pnl.fontTable[sel];

        pnl.style.removeAll();

        var styles = family.styles;

        for (var i = 0; i < styles.length; i++) {
            var it = pnl.style.add('item', styles[i].style);
            it.font = styles[i].font;
        }
        if (pnl._defaultStyle) {
            var it = pnl.style.find(pnl._defaultStyle);
            pnl._defaultStyle = undefined;
            if (it) {
                it.selected = true;
            } else {
                pnl.style.items[0].selected = true;
            }
        } else {
            pnl.style.items[0].selected = true;
        }
    };
    pnl.family.items[0].selected = true;

    //we have used editnumber control for fontSize, so blocking of invalid input will be done internally
    //pnl.fontSize.onChanging = psxui.numericKeystrokeFilter;

    //
    // Function: setFont
    // Description: set the font and font size
    // Input: str  - TextFont or the font name
    //        size - the font size in points 
    // Return: <none>
    //
    pnl.setFont = function (str, size) {
        var pnl = this;
        if (!str) {
            return;
        }
        var font = (str.typename == "TextFont") ? str : psx.determineFont(str);
        if (!font) {
            font = psx.getDefaultFont();
        }
        if (font) {
            var it = pnl.family.find(font.family);
            if (it) {
                it.selected = true;
                pnl._defaultStyle = font.style;
            }
        }
        pnl.fontSize.value = size;
        pnl._fontSize = size;
        pnl.family.onChange();
    };

    //
    // Function: getFont
    // Description: Gets the current font and font size
    // Input:  <none> 
    // Return: an object containing the font and size
    //
    pnl.getFont = function () {
        var pnl = this;
        var font = pnl.style.selection.font;
        return { font: font.postScriptName, size: pnl.fontSize.value };

        var fsel = pnl.family.selection.text;
        var ssel = pnl.style.selection.text;
        var family = pnl.fontTable[sel];
        var styles = familyStyles;
        var font = undefined;

        for (var i = 0; i < styles.length && font == undefined; i++) {
            if (styles[i].style == ssel) {
                font = styles[i].font;
            }
        }
        return { font: font, size: toNumber(font.fontSize) };
    }

    return pnl;
};

//
// Function: _getFontTable
// Description: Used by the Font Panel. Creates a table that
//              maps font names to their styles
// Input:  <none> 
// Return: an object where the names are font.family and the
//         values are objects containing the font family and styles
//
psxui._getFontTable = function () {
    var fonts = app.fonts;
    var fontTable = {};
    for (var i = 0; i < fonts.length; i++) {
        var font = fonts[i];
        var entry = fontTable[font.family];
        if (!entry) {
            entry = { family: font.family, styles: [] };
            fontTable[font.family] = entry;
        }
        entry.styles.push({ style: font.style, font: font });
    }
    return fontTable;
};

//
// Function: _getFontArray
// Description: 
// Input:  <none> 
// Return: an array of font info objects created by _getFontTable
//
psxui._getFontArray = function () {
    var fontTable = psxui._getFontTable();
    var fonts = [];
    for (var idx in fontTable) {
        var f = fontTable[idx];
        fonts.push(f);
    }
    return fonts;
};

//
// Function: createProgressPalette
// Description: Opens up a palette window with a progress bar that can be
//              'asynchronously' while the script continues running
// Input:
//   title     the window title
//   min       the minimum value for the progress bar
//   max       the maximum value for the progress bar
//   parent    the parent ScriptUI window (opt)
//   useCancel flag for having a Cancel button (opt)
//   msg        message that can be displayed and changed in the palette (opt)
//
//   onCancel  This method will be called when the Cancel button is pressed.
//             This method should return 'true' to close the progress window
// Return: The palette window
//
psxui.createProgressPalette = function (title, min, max,
    parent, useCancel, msg) {
    var opts = {
        closeButton: false,
        maximizeButton: false,
        minimizeButton: false
    };
    var win = new Window('palette', title, undefined, opts);
    win.bar = win.add('progressbar', undefined, min, max);
    if (msg) {
        win.msg = win.add('statictext');
        win.msg.text = msg;
    }
    win.bar.preferredSize = [500, 20];

    win.parentWin = undefined;
    win.recenter = false;
    win.isDone = false;

    if (parent) {
        if (parent instanceof Window) {
            win.parentWin = parent;
        } else if (useCancel == undefined) {
            useCancel = !!parent;
        }
    }

    if (useCancel) {
        win.onCancel = function () {
            this.isDone = true;
            return true;  // return 'true' to close the window
        };

        win.cancel = win.add('button', undefined, ZStrs.Cancel);

        win.cancel.onClick = function () {
            var win = this.parent;
            try {
                win.isDone = true;
                if (win.onCancel) {
                    var rc = win.onCancel();
                    if (rc != false) {
                        if (!win.onClose || win.onClose()) {
                            win.close();
                        }
                    }
                } else {
                    if (!win.onClose || win.onClose()) {
                        win.close();
                    }
                }
            } catch (e) {
                LogFile.logException(e, '', true);
            }
        };
    }

    win.onClose = function () {
        this.isDone = true;
        return true;
    };

    win.updateProgress = function (val) {
        var win = this;

        if (val != undefined) {
            win.bar.value = val;
        }
        //     else {
        //       win.bar.value++;
        //     }

        if (win.recenter) {
            win.center(win.parentWin);
        }

        win.update();
        win.show();
        // win.hide();
        // win.show();
    };

    win.recenter = true;
    win.center(win.parent);

    return win;
};

"psx.jsx";
// EOF

//
//
// CSIIConst
//
// $Id: CSIIConst.jsx,v 1.6 2012/03/20 21:06:36 anonymous Exp $
//
//
//@show include
//
//

try {
    var _lvl = $.level;
    $.level = 0;
    ZStrings;
} catch (e) {
    ZStrings = {};
} finally {
    $.level = _lvl;
    delete _lvl;
}

// Setup locale-specific strings

ZStrings.Photoshop =
    localize("$$$/AETE/Common/Photoshop=Photoshop");

ZStrings.Bridge =
    localize("$$$/JavaScripts/ContactSheet2/Bridge=Bridge");

ZStrings.SourceIntro =
    localize("$$$/JavaScripts/ContactSheet2/SourceIntro=Select files for Contact Sheet(s)");

ZStrings.LoadSettingsFile =
    localize("$$$/JavaScripts/ContactSheet2/LoadSettingsFile=Load Contact Sheet Settings");

ZStrings.SaveSettingsFile =
    localize("$$$/JavaScripts/ContactSheet2/SaveSettingsFile=Save Contact Sheet Settings");

ZStrings.XMLFileDlgPattern =
    localize("$$$/JavaScripts/ContactSheet2/XMLFileDlgPattern=XML File: *.xml");

ZStrings.SettingsForCSII =
    localize("$$$/JavaScripts/ContactSheet2/SettingsForCSII=Settings for Auto-Save-to-TIFF-From-Contact-Sheet-II");

ZStrings.ConfirmReset =
    localize("$$$/JavaScripts/ContactSheet2/ConfirmReset=Restore default settings?");

ZStrings.DefaultSettingsMissingErr =
    localize("$$$/JavaScripts/ContactSheet2/DefaultSettingsMissingErr=The default settings file is missing");

ZStrings.ContactSheetIIName =
    localize("$$$/AdobePlugin/ContactSheet2/Name=Auto-Save-to-TIFF-From-Contact-Sheet-II");

ZStrings.CSIIFileName = ZStrings.ContactSheetIIName;

ZStrings.ContactSheetIIPluginName =
    localize("$$$/AdobePlugin/PIPLInfo/PluginName/ContactSheetII=Auto-Save-to-TIFF-From-Contact-Sheet-II...");

ZStrings.ContactSheetIIUIError =
    localize("$$$/JavaScripts/ContactSheet2/ContactSheetIIUIError=Error in ContactSheet II UI");

ZStrings.ContactSheetIIProcessingError =
    localize("$$$/JavaScripts/ContactSheet2/ContactSheetIIProcessingError=Error in ContactSheet Processing");

ZStrings.ContactSheetIISettingsError =
    localize("$$$/JavaScripts/ContactSheet2/ContactSheetIISettingsError=Error saving ContactSheet settings");

ZStrings.ErrorInProcessing =
    localize("$$$/JavaScripts/ContactSheet2/ErrorInProcessing=Error in processing: ");

ZStrings.BadRecordingFmt =
    localize("$$$/JavaScripts/ContactSheet2/BadRecordingFmt=Bad %%s information recorded in action.");

ZStrings.BadFolder =
    localize("$$$/JavaScripts/ContactSheet2/BadFolder=folder");

ZStrings.BadSource =
    localize("$$$/JavaScripts/ContactSheet2/BadSource=source");

ZStrings.BadUnits =
    localize("$$$/JavaScripts/ContactSheet2/BadUnits=Units");

ZStrings.BadResolution =
    localize("$$$/JavaScripts/ContactSheet2/BadResolution=Resolution Units");

ZStrings.BadColorMode =
    localize("$$$/JavaScripts/ContactSheet2/BadColorMode=Color Mode");

ZStrings.ProfileConversionError =
    localize("$$$/JavaScripts/ContactSheet2/ProfileConversionError=Unknown color profile error");

ZStrings.BridgeFilesNotSelected =
    localize("$$$/JavaScripts/ContactSheet2/BridgeFilesNotSelected=Files not selected in Bridge");

//
// Source Images Panel
//
ZStrings.SourceImages =
    localize("$$$/JavaScripts/ContactSheet2/SourceImages=Source Images");

ZStrings.SourcePnlBtnWidth =
    localize("$$$/locale_specific/JavaScripts/ContactSheet2/SourcePnlBtnWidth=120");

ZStrings.UseLabel =
    localize("$$$/JavaScripts/ContactSheet2/UseLabel=Use:");

ZStrings.UseTip =
    localize("$$$/JavaScripts/ContactSheet2/UseTip=Select whether to use files or a folder of images");

ZStrings.CurrentDocs =
    localize("$$$/JavaScripts/ContactSheet2/CurrentDocs=Open Documents");

ZStrings.Directories =
    localize("$$$/JavaScripts/ContactSheet2/Directories=Directories");

ZStrings.Files =
    localize("$$$/JavaScripts/ContactSheet2/Files=Files");

ZStrings.Folder =
    localize("$$$/JavaScripts/ContactSheet2/Folder=Folder");

ZStrings.Browse =
    localize("$$$/JavaScripts/ContactSheet2/Browse=Browse...");

ZStrings.Remove =
    localize("$$$/JavaScripts/ContactSheet2/Remove=Remove");

ZStrings.AddOpenFiles =
    localize("$$$/JavaScripts/ContactSheet2/AddOpenFiles=Add Open Files");

ZStrings.SortByFilename =
    localize("$$$/JavaScripts/ContactSheet2/SortByFilnemame=Sort By Name");

ZStrings.AddBridgeFiles =
    localize("$$$/JavaScripts/ContactSheet2/AddBridgeFiles=Add Bridge Files");

ZStrings.Choose =
    localize("$$$/JavaScripts/ContactSheet2/Choose=Choose...");

ZStrings.ChooseSourceFolderTip =
    localize("$$$/JavaScripts/ContactSheet2/ChooseSourceFolderTip=Select the folder of images to use");

ZStrings.IncludeSubfolders =
    localize("$$$/JavaScripts/ContactSheet2/IncludeSubfolders=Include Subfolders");

ZStrings.IncludeSubfoldersTip =
    localize("$$$/JavaScripts/ContactSheet2/IncludeSubfoldersTip=Include all images in all subfolders");

ZStrings.GroupByFolder =
    localize("$$$/JavaScripts/ContactSheet2/GroupByFolder=Group Images by Folder");

ZStrings.GroupByFolderTip =
    localize("$$$/JavaScripts/ContactSheet2/GroupByFolderTip=Images in subfolders will start on a new Contact Sheet");

ZStrings.FilesSelectedFmt =
    localize("$$$/JavaScripts/ContactSheet2/FilesSelectedFmt=%%d files selected");

ZStrings.FileSelectedFmt =
    localize("$$$/JavaScripts/ContactSheet2/FileSelectedFmt=%%d file selected");

//
// Document Panel
//
ZStrings.Document =
    localize("$$$/JavaScripts/ContactSheet2/Document=Document");

ZStrings.DocumentPnlCol2 =
    localize("$$$/locale_specific/JavaScripts/ContactSheet2/DocumentPnlCol2=90");

ZStrings.UnitsLabel =
    localize("$$$/JavaScripts/ContactSheet2/UnitsLabel=Units:");

ZStrings.UnitsTip =
    localize("$$$/JavaScripts/ContactSheet2/UnitsTip=Select the unit");

ZStrings.WidthLabel =
    localize("$$$/JavaScripts/ContactSheet2/WidthLabel=Width:");

ZStrings.Width =
    localize("$$$/JavaScripts/ContactSheet2/Width=Width");

ZStrings.WidthTip =
    localize("$$$/JavaScripts/ContactSheet2/WidthTip=Select the width");

ZStrings.HeightLabel =
    localize("$$$/JavaScripts/ContactSheet2/HeightLabel=Height:");

ZStrings.Height =
    localize("$$$/JavaScripts/ContactSheet2/Height=Height");

ZStrings.HeightTip =
    localize("$$$/JavaScripts/ContactSheet2/HeightTip=Select the height");

ZStrings.ResolutionLabel =
    localize("$$$/JavaScripts/ContactSheet2/ResolutionLabel=Resolution:");

ZStrings.Resolution =
    localize("$$$/JavaScripts/ContactSheet2/Resolution=Resolution");

ZStrings.ResolutionTip =
    localize("$$$/JavaScripts/ContactSheet2/ResolutionTip=Select the resolution");

ZStrings.ModeLabel =
    localize("$$$/JavaScripts/ContactSheet2/ModeLabel=Mode:");

ZStrings.Mode =
    localize("$$$/JavaScripts/ContactSheet2/Mode=Mode");

ZStrings.ModeTip =
    localize("$$$/JavaScripts/ContactSheet2/ModeTip=Select the color mode");

ZStrings.BitDepthLabel =
    localize("$$$/JavaScripts/ContactSheet2/BitDepthLabel=Bit Depth:");

ZStrings.BitDepthTip =
    localize("$$$/JavaScripts/ContactSheet2/BitDepthTip=Select the bit depth");

ZStrings.ColorProfileLabel =
    localize("$$$/JavaScripts/ContactSheet2/ColorProfileLabel=Color Profile:");

ZStrings.ColorProfileTip =
    localize("$$$/JavaScripts/ContactSheet2/ColorProfileTop=Select the color profile");

ZStrings.FlattenAllLayers =
    localize("$$$/JavaScripts/ContactSheet2/FlattenAllLayers=Flatten All Layers");

ZStrings.FlattenAllLayersTip =
    localize("$$$/JavaScripts/ContactSheet2/FlattenAllLayersTip=Flattened images will only have a background layer");

ZStrings.Flattened =
    localize("$$$/JavaScripts/ContactSheet2/Flattened=Flattened Layers");


ZStrings.Centimeter =
    localize("$$$/JavaScripts/ContactSheet2/Centimeter=cm");

ZStrings.Inches =
    localize("$$$/JavaScripts/ContactSheet2/Inches=inches");

ZStrings.Pixels =
    localize("$$$/JavaScripts/ContactSheet2/Pixels=pixels");

ZStrings.PixelsPerInch =
    localize("$$$/JavaScripts/ContactSheet2/Pixels/Inch=pixels/inch");

ZStrings.PixelsPerCM =
    localize("$$$/JavaScripts/ContactSheet2/Pixels/cm=pixels/cm");

ZStrings.CMYKMode = ZStrs.CMYKMode;

ZStrings.GrayscaleMode = ZStrs.GrayscaleMode;

ZStrings.LabMode = ZStrs.LabMode;

ZStrings.RGBMode = ZStrs.RGBMode;

ZStrings.BitDepth8 =
    localize("$$$/Info/Depth/8Bit=8-bit");

ZStrings.BitDepth16 =
    localize("$$$/Info/Depth/16Bit=16-bit");

//
// Thumbnails Panel
//
ZStrings.Thumbnails =
    localize("$$$/JavaScripts/ContactSheet2/Thumbnails=Thumbnails");

ZStrings.PlaceLabel =
    localize("$$$/JavaScripts/ContactSheet2/PlaceLabel=Place:");

ZStrings.PlaceTip =
    localize("$$$/JavaScripts/ContactSheet2/PlaceTip=Select which direction the images appear on the Contact Sheet");

ZStrings.AcrossFirst =
    localize("$$$/JavaScripts/ContactSheet2/AcrossFirst=across first");

ZStrings.DownFirst =
    localize("$$$/JavaScripts/ContactSheet2/DownFirst=down first");

ZStrings.ColumnsLabel =
    localize("$$$/JavaScripts/ContactSheet2/ColumnsLabel=Columns:");

ZStrings.Columns =
    localize("$$$/JavaScripts/ContactSheet2/Columns=Columns");

ZStrings.ColumnsTip =
    localize("$$$/JavaScripts/ContactSheet2/ColumnsTip=Enter the number of columns");

ZStrings.RowsLabel =
    localize("$$$/JavaScripts/ContactSheet2/RowsLabel=Rows:");

ZStrings.Rows =
    localize("$$$/JavaScripts/ContactSheet2/Rows=Rows");

ZStrings.RowsTip =
    localize("$$$/JavaScripts/ContactSheet2/RowsTip=Enter the number of rows");

ZStrings.NumberOfColumns =
    localize("$$$/JavaScripts/ContactSheet2/NumberOfColumns=number of columns");

ZStrings.NumberOfRows =
    localize("$$$/JavaScripts/ContactSheet2/NumberOfRows=number of rows");

ZStrings.UseAutoSpacing =
    localize("$$$/JavaScripts/ContactSheet2/UseAutoSpacing=Use Auto-Spacing");

ZStrings.UseAutoSpacingTip =
    localize("$$$/JavaScripts/ContactSheet2/UseAutoSpacingTip=Select spacing between images");

ZStrings.VerticalLabel =
    localize("$$$/JavaScripts/ContactSheet2/VerticalLabel=Vertical:");

ZStrings.Vertical =
    localize("$$$/JavaScripts/ContactSheet2/Vertical=Vertical");

ZStrings.VerticalTip =
    localize("$$$/JavaScripts/ContactSheet2/VerticalTip=Enter vertical spacing between images");

ZStrings.VerticalSpacing =
    localize("$$$/JavaScripts/ContactSheet2/VerticalSpacing=Vertical Spacing");

ZStrings.HorizontalLabel =
    localize("$$$/JavaScripts/ContactSheet2/HorizontalLabel=Horizontal:");

ZStrings.Horizontal =
    localize("$$$/JavaScripts/ContactSheet2/Horizontal=Horizontal");

ZStrings.HorizontalTip =
    localize("$$$/JavaScripts/ContactSheet2/HorizontalTip=Enter horizontal spacing between images");

ZStrings.HorizontalSpacing =
    localize("$$$/JavaScripts/ContactSheet2/HorizontalSpacing=Horizontal Spacing");

ZStrings.RotateForBestFit =
    localize("$$$/JavaScripts/ContactSheet2/RotateForBestFit=Rotate For Best Fit");

ZStrings.RotateForBestFitTip =
    localize("$$$/JavaScripts/ContactSheet2/RotateForBestFitTip=Rotate images when necessary for a better fit");


ZStrings.PlaceAcrossFirst =
    localize("$$$/JavaScripts/ContactSheet2/PlaceAcrossFirst=place across first");

ZStrings.ResolutionUnit =
    localize("$$$/JavaScripts/ContactSheet2/ResolutionUnit=Resolution Unit");

ZStrings.SheetHeight =
    localize("$$$/JavaScripts/ContactSheet2/SheetHeight=sheet height");

ZStrings.SheetWidth =
    localize("$$$/JavaScripts/ContactSheet2/SheetWidth=sheet width");

ZStrings.Source2 =
    localize("$$$/JavaScripts/ContactSheet2/Source2=source");

ZStrings.Source =
    localize("$$$/JavaScripts/ContactSheet2/Source=source");

ZStrings.WidthHeightUnit =
    localize("$$$/JavaScripts/ContactSheet2/WidthHeightUnit=Width Height Unit");

ZStrings.ContactSheet2Name =
    localize("$$$/AdobePlugin/ContactSheet2/Name=Auto-Save-to-TIFF-From-Contact-Sheet-II");


//
// Caption Panel
//
ZStrings.FilenameAsCaption =
    localize("$$$/JavaScripts/ContactSheet2/FilenameAsCaption=Use Filename as Caption");

ZStrings.FilenameCaption =
    localize("$$$/JavaScripts/ContactSheet2/FilenameCaption=filename caption");

ZStrings.Font =
    localize("$$$/JavaScripts/ContactSheet2/Font=font");

ZStrings.FontSize =
    localize("$$$/JavaScripts/ContactSheet2/FontSize=Font Size");

ZStrings.MacCourier =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Mac/Courier=Courier");
// was AETE/ContactSheet2/Mac/Courier=Courier
// mapped to Myriad Pro for en_*

ZStrings.MacSansSerif =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Mac/Helvetica=Helvetica");
// was AETE/ContactSheet2/Mac/SansSerif=Helvetica
// mapped to Minion Pro for en_*

ZStrings.MacSansSerifFaceName =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFontFaceName/Mac/Helvetica=Helvetica");

ZStrings.MacSerif =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Mac/TimesNewRoman=Times New Roman");
// was AETE/ContactSheet2/Mac/Serif=Times
// mapped to Lucida Grande for en_*

ZStrings.WinCourier =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Windows/Courier=Courier");
// was AETE/ContactSheet2/Win/Courier=Courier

ZStrings.WinSansSerif =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Windows/Arial=Arial")
// was AETE/ContactSheet2/Win/SansSerif=Arial

ZStrings.WinSansSerifFaceName =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFontFaceName/Windows/Arial=Arial")

ZStrings.WinSerif =
    localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Windows/TimesNewRoman=Times New Roman");
// was AETE/ContactSheet2/Win/Serif=Times New Roman


ZStrings.FontArial =
    localize("$$$/JavaScripts/ContactSheet2/FontArial=ArialMT");

ZStrings.ContactSheetFilenameFormat =
    localize("$$$/JavaScripts/ContactSheet2/ContactSheetFilenameFormat=ContactSheet-%%03d");

ZStrings.FolderSelect =
    localize("$$$/JavaScripts/ContactSheet2/FolderSelect=Select folder");

ZStrings.BrowseForFolder =
    localize("$$$/JavaScripts/ContactSheet2/BrowseForFolder=Browse For Folder");

ZStrings.NoFolderSelected =
    localize("$$$/JavaScripts/ContactSheet2/NoFolderSelected=[Select Image Folder]");

ZStrings.InvalidValueMsg =
    localize("$$$/JavaScripts/ContactSheet2/InvalidValueMsg=Invalid value for %%s. Last good value inserted.");

ZStrings.InvalidNumberMsg =
    localize("$$$/JavaScripts/ContactSheet2/InvalidNumberMsg=Invalid numeric value for %%s. Last good value inserted.");

ZStrings.NumberOutOfRangeMsg =
    localize("$$$/JavaScripts/ContactSheet2/NumberOutOfRangeMsg=%%s is out of range.^r^nPlease specify a number between %%.03f and %%.03f. Closest value inserted.");

ZStrings.BadLegacyFont =
    localize("$$$/JavaScripts/ContactSheet2/BadLegacyFont=Bad font in Legacy Action");

ZStrings.UnknownInternalError =
    localize("$$$/JavaScripts/ContactSheet2/UnknownInternalError=Unknown Internal Error");

ZStrings.UserCancelledProcessing =
    localize("$$$/JavaScripts/ContactSheet2/UserCancelledProcessing=User cancelled processing.");

ZStrings.OK =
    localize("$$$/JavaScripts/ContactSheet2/OK=OK");

ZStrings.Cancel =
    localize("$$$/JavaScripts/ContactSheet2/Cancel=Cancel");

ZStrings.Load =
    localize("$$$/JavaScripts/ContactSheet2/Load=Load...");

ZStrings.Save =
    localize("$$$/JavaScripts/ContactSheet2/Save=Save...");

ZStrings.Reset =
    localize("$$$/JavaScripts/ContactSheet2/Reset=Reset...");

ZStrings.ProcessingSheets =
    localize("$$$/JavaScripts/ContactSheet2/ProcessingSheets=Auto-Save-to-TIFF-From-Contact-Sheet-II - Processing %%d of %%d sheets...");

ZStrings.PressESC =
    localize("$$$/JavaScripts/ContactSheet2/PressESC=Press the ESC key to Cancel processing images");

ZStrings.VersionWarning =
    localize("$$$/JavaScripts/ContactSheet2/VersionWarning=This script requires at least version %%d of Photoshop");

ZStrings.LogFileReference =
    localize("$$$/JavaScripts/ContactSheet2/LogFileReference=More information can be found in the log file:");

ZStrings.ErrorDetailsPrompt =
    localize("$$$/JavaScripts/ContactSheet2/ErrorDetailsPrompt=There was an error. See details?");

ZStrings.NoSettingsError =
    localize("$$$/JavaScripts/ContactSheet2/NoSettingsError=No settings available");

ZStrings.NoImageFiles =
    localize("$$$/JavaScripts/ContactSheet2/NoImageFiles=No image files were found.");

ZStrings.ImageDoesNotExistFmt =
    localize("$$$/JavaScripts/ContactSheet2/ImagDoesNotExistFmt=Image %%s does not exist.");

ZStrings.SelectAnImageFolderWarning =
    localize("$$$/JavaScripts/ContactSheet2/SelectAnImageFolderWarning=Select an Image Folder");

ZStrings.SelectImageFilesWarning =
    localize("$$$/JavaScripts/ContactSheet2/SelectImageFilesWarning=Please select image files for processing");

ZStrings.ReadSettingsError =
    localize("$$$/JavaScripts/ContactSheet2/ReadSettingsError=Error reading settings: ");

ZStrings.ActionRecordingError =
    localize("$$$/JavaScripts/ContactSheet2/ActionRecordingError=Internal Error. Unable to record as an Action step: ");

ZStrings.ErrorsDetectedFmt =
    localize("$$$/JavaScripts/ContactSheet2/ErrorsDetectedFmt=%%d errors were detected.");

ZStrings.CreateFailure =
    localize("$$$/JavaScripts/ContactSheet2/CreateFailure=Unable create new Contact Sheet document.^rPlease check your document dimensions and units");

ZStrings.InvalidThumbnailSize =
    localize("$$$/JavaScripts/ContactSheet2/InvalidThumbnailSize=The values you have entered for document width or height, thumbnail columns or rows and font size produce an invalid thumbnail size.");


// CSII is a namespace for constants and enumerations used in this script
CSII = function () { };

CSII.UUID = "0B71D221-F8CE-11d2-B21B-0008C75B322Z";

CSII.CONTACT_SHEET = ZStrings.ContactSheetIIName;
CSII.CS_LAYER_NAME = CSII.CONTACT_SHEET;

CSII.RELEASE = "2.2.14";

CSII.FILE = CSII.CONTACT_SHEET + CSII.RELEASE.replace(/\./g, '_') + ".jsx";

CSII.VERSION = "v" + CSII.RELEASE;
CSII.REVISION = "$Revision: 1.6 $";
CSII.TITLE = ZStrings.ContactSheetIIName;

CSII.CW = 'CW';
CSII.CCW = 'CCW';

CSII.TOP = 'Top';
CSII.MIDDLE = 'Middle';
CSII.BOTTOM = 'Bottom';

CSII.LEFT = 'Left';
CSII.CENTER = 'Center';
CSII.RIGHT = 'Right';

CSII.NO_CAPTION_STYLE = 'None';
CSII.NO_THUMB_STYLE = 'None';

CSII.REQUIRED_PS_VERSION = 12;

CSII.kForceRecording = sTID('forceRecording');
CSII.kCSIISettings = sTID('CSIISettings');
CSII.kFilesList = sTID('filesList');
CSII.kMessage = cTID('Msge');
CSII.kFileOpenError = 9050;

// Controls whether or not a flattened sheet has an
// empty background layer with the sheet (true) or not (false)
CSII.USE_LEGACY_FLATTEN = false;


// CSIIDefaults are values for initially populating the UI
CSIIDefaults = {};

// Source settings
CSIIDefaults.imageSource = ZStrings.Folder;
CSIIDefaults.path = "";
CSIIDefaults.includeSubfolders = true;
CSIIDefaults.groupImages = false;


// Document settings
CSIIDefaults.units = ZStrings.Inches;
CSIIDefaults.width = 8;
CSIIDefaults.height = 10;
CSIIDefaults.resolution = 300;
CSIIDefaults.resUnits = ZStrings.PixelsPerInch;
CSIIDefaults.mode = ZStrings.RGBMode;
CSIIDefaults.bitDepth = ZStrings.BitDepth8;
CSIIDefaults.colorProfile = ZStrs.ProfileSRGB;
CSIIDefaults.flattenLayers = true;

CSIIDefaults.pixelsPerInch = "pixels/inch"
CSIIDefaults.pixelsPerInch = "pixels/cm";

// Thumbnail settings
CSIIDefaults.place = ZStrings.AcrossFirst;
CSIIDefaults.cols = 5;
CSIIDefaults.rows = 6;
CSIIDefaults.bestFit = false;
CSIIDefaults.useAutospacing = true;
CSIIDefaults.vert = 0.014;  // 1 px @ 72 ppi
CSIIDefaults.horz = 0.014;  // 1 px @ 72 ppi

CSIIDefaults.acrossFirst = "across first"
CSIIDefaults.downFirst = "across first"


// Caption settings
CSIIDefaults.captionEnabled = true;
CSIIDefaults.fontName = (isWindows() ?
    ZStrings.WinSansSerif : ZStrings.MacSansSerif);
CSIIDefaults.fontFaceName = (isWindows() ?
    ZStrings.WinSansSerifFaceName :
    ZStrings.MacSansSerifFaceName);
CSIIDefaults.fontSize = 12;

CSIIDefaults.OUTPUT_PREFIX = "sheet_";
CSIIDefaults.FILENAME_FMT = "sheet_%03d";


// IDs for Legacy interface
Keys = {};
Keys.InputLocation = cTID('InpD');
Keys.OutputLocation = cTID('OutD');
Keys.UseFrontmostDoc = cTID('Frnt');
Keys.DocType = cTID('DcTy');
Keys.DocDest = cTID('DcDs');
Keys.ThumbSize = cTID('ThSz');
Keys.LayoutFile = cTID('Lyt ');
Keys.PageSize = cTID('PgSz');
Keys.Layout = cTID('Lytt');
Keys.OverrideFileList = cTID('ovfl');
Keys.IncludeName = cTID('IndN');
Keys.IncludeSubDir = cTID('InSd');
Keys.Font = cTID('Font');
Keys.FontSize = cTID('FtSz');
Keys.InputFile = cTID('InpF');
Keys.Columns = cTID('Cols');
Keys.Rows = cTID('Rows');
Keys.RowOrdered = cTID('RowO');
Keys.ResolutionUnits = cTID('RsUn');
Keys.Units = cTID('Unit');
Keys.UseAutoSpacing = cTID('UAS ');
Keys.HorizontalSpacing = cTID('HzSp');
Keys.VerticalSpacing = cTID('VtSp');
Keys.InputSource = cTID('InSr');
Keys.RotateForBestFit = cTID('Rfbf');
Keys.FlattenText = cTID('FltT');
Keys.Width = cTID('Wdth');
Keys.Height = cTID('Hght');
Keys.Resolution = cTID('Rslt');
Keys.Mode = cTID('Md  ');

Enums = {};
Enums.CurrentDocs = cTID('cudo');
Enums.Folder = cTID('fold');
Enums.FileBrowser = cTID('flbr');
Enums.Files = cTID('file');

Enums.UnitInches = cTID('Inch');
Enums.UnitCentimeter = cTID('Ctmr');
Enums.UnitPixels = cTID('Pixl');

Enums.PixelsPerInch = cTID('PpIn');
Enums.PixelsPerCentimeter = cTID('PpCm');

Enums.CMYKMode = cTID('ECMY');
Enums.GrayscaleMode = cTID('Grys');
Enums.LabMode = cTID('LbCl');
Enums.RGBMode = cTID('RGBC');

Enums.SansSerif = cTID('Sans');
Enums.Serif = cTID('Serf');
Enums.Courier = cTID('Cour');

"CSIIConst.jsx";
// EOF

//
// CSIILayout
//
// $Id: CSIILayout.jsx,v 1.9 2012/03/03 01:06:52 anonymous Exp $
//
//
//@show include
//
//

//
// ContactSheetIIOptions - these are the settings used for
//                         creating sheets.
//
ContactSheetIIOptions = function ContactSheetIIOptions(obj) {
    var self = this;

    self._version = CSII.VERSION;
    self._revision = CSII.REVISION;
    self._appName = app.name;
    self._appVersion = app.version;
    self._os = $.os;

    // Source Images - 
    // Files, Folder,Open Documents, Bridge
    self.imageSource = CSIIDefaults.imageSource;
    self.folder = CSIIDefaults.path;
    self.recurse = CSIIDefaults.includeSubfolders;
    self.spanFolders = !CSIIDefaults.groupImages;

    // Document
    self.background = ZStrs.white;
    // pixles, inches, cm
    self.docUnitsLocalized = CSIIDefaults.units;
    self.docUnits = delocalizeUnitType(CSIIDefaults.units);
    self.docWidth = CSIIDefaults.width;      // 100..20000 fix
    self.docHeight = CSIIDefaults.height;     // 100..20000 fix
    self.resolution = CSIIDefaults.resolution; // 25..1200 fix
    // pixels/in, pixels/cm
    self.resUnitsLocalized = CSIIDefaults.resUnitsLocalized;
    self.resUnits = delocalizeUnitType(CSIIDefaults.resUnits);
    self.modeLocalized = CSIIDefaults.mode;
    self.mode = ContactSheetIIOptions.toMode(CSIIDefaults.mode);
    self.colorProfile = CSIIDefaults.colorProfile;

    // Layout
    self.acrossFirst = (CSIIDefaults.place == ZStrings.AcrossFirst);
    self.bestFit = CSIIDefaults.bestFit;
    self.rotate = CSII.CCW;       // CW,CCW
    self.autoFill = false;
    self.valign = CSII.MIDDLE;       // Top,Middle,Bottom
    self.halign = CSII.CENTER;    // Left,Center,Right
    self.rowCount = CSIIDefaults.rows;           // 1..30 fix
    self.columnCount = CSIIDefaults.cols;           // 1..30 fix
    self.useAutoSpacing = CSIIDefaults.useAutospacing;
    self.spacingUnits = CSIIDefaults.units;        // all units
    self.vertical = CSIIDefaults.vert;         // 0..1000 fix
    self.horizontal = CSIIDefaults.horz;         // 0..1000 fix

    // Caption
    self.caption = CSIIDefaults.captionEnabled;
    self.captionFilename = true;
    self.captionFormat = "%f.%e";
    self.noExtensions = false;
    self.font = CSIIDefaults.fontName;
    self.fontFaceName = CSIIDefaults.fontFaceName;
    self.fontSize = CSIIDefaults.fontSize;
    self.fontColor = ZStrs.black;

    // enable using ... for truncated captions
    self.dotTruncate = true;

    self.captionStyle = CSII.NO_CAPTION_STYLE;
    self.overlayCaption = false;

    // Output
    self.keepOpen = false;
    self.saveSheet = false;
    self.outputFolder = "~";
    self.flatten = true;

    self.nextPage = false;
    self.nextFile = false;
    self._pageStart = 1;
    self._pagePad = 2;
    self._outputStart = 1;
    self._outputPad = 3;
    self.outputPrefix = CSIIDefaults.OUTPUT_PREFIX;
    self.fileNameFormat = CSIIDefaults.FILENAME_FMT;

    self.fileSaveType = "jpg";        // bmp,gif,jpg,psd,eps,pdf,png,tga,tiff

    // Advanced
    self.autospaceFactor = 1.0;   // percent of width

    self.hiperfMode = 1; // needs to be removed

    self.forceRecording = true;
    self.xpSort = false;

    self.autoscaleCaptions = true;

    psx.copyFromTo(obj, self);
};


ContactSheetIIOptions.prototype.toString = function () { return this.typename; };
ContactSheetIIOptions.prototype.typename = "ContactSheetIIOptions";

ContactSheetIIOptions.SETTINGS_FOLDER = Folder.userData;

ContactSheetIIOptions.INI_FILE = (ContactSheetIIOptions.SETTINGS_FOLDER +
    "/" + ZStrings.CSIIFileName + ".ini");
ContactSheetIIOptions.LOG_FILE = (ContactSheetIIOptions.SETTINGS_FOLDER +
    "/ContactSheetII.log");
ContactSheetIIOptions.UI_LOG_FILE = (ContactSheetIIOptions.SETTINGS_FOLDER +
    "/ContactSheetII-UI.log");
ContactSheetIIOptions.SETTINGS_FILE_NAME = ZStrings.CSIIFileName + ".xml";


ContactSheetIIOptions.DEFAULT_XML = new XML("<ContactSheetIISettings/>");

ContactSheetIIOptions.SHORT_FILENAME_LENGTH = 43;

ContactSheetIIOptions.LOG_ENABLED = true;
ContactSheetIIOptions.PAGE_PAD = 2;
ContactSheetIIOptions.FILE_PAD = 4;

//
// Function: toMode
// Description: convert a possibly localized mode string into something useful
// Input: a mode string
// Return: a NewDocumentMode value or undefined if no mapping is found
//
ContactSheetIIOptions.toMode = function (str) {
    str = str.toString();
    if (str.match(/rgb/i) || str == ZStrings.RGBMode) {
        return NewDocumentMode.RGB;
    }
    if (str.match(/cmyk/i) || str == ZStrings.CMYKMode) {
        return NewDocumentMode.CMYK;
    }
    if (str.match(/lab/i) || str == ZStrings.LabMode) {
        return NewDocumentMode.LAB;
    }
    if (str.match(/grayscale/i) || str == ZStrings.GrayscaleMode) {
        return NewDocumentMode.GRAYSCALE;
    }

    return undefined;
};

//
// Function: toModeStr
// Description: convert some kind of "mode" value to a EN string
// Input: a "mode" value
// Return: an EN representation of the mode
//
ContactSheetIIOptions.toModeStr = function (str) {
    var mode = ContactSheetIIOptions.toMode(str);
    var modeStr = undefined;
    switch (mode) {
        case NewDocumentMode.RGB: modeStr = "RGB"; break;
        case NewDocumentMode.CMYK: modeStr = "CMYK"; break;
        case NewDocumentMode.LAB: modeStr = "Lab"; break;
        case NewDocumentMode.GRAYSCALE: modeStr = "Grayscale"; break;
    }

    return modeStr;
};


//
// Function: toFont
// Description: convert a string to a canonical Font name
// Input: a string
// Return: a canonical Font name
//
ContactSheetIIOptions.toFont = function (str) {
    str = str.toString();

    var f = psx.determineFont(str);  // first, check by PS name

    return (f ? f.postScriptName : undefined);
};

//
// Placeholder for a function that would rationalized/validate
// a ContactSheetIIOptions object
//
ContactSheetIIOptions.prototype.rationalize = function () {
    var self = this;

    Error.runtimeError(9500, ZStrings.UnknownInternalError);

    return self;
};



//
// ContactSheetUI - these settings are used for control of the CSII UI
//
ContactSheetUI = function ContactSheetUI(obj) {
    var self = this;

    self.title = CSII.TITLE;
    self.winRect = {   // the rect for the Standard window
        x: 100,
        y: 100,
        w: ContactSheetUI.CSII_WIDTH,
        h: ContactSheetUI.CSII_HEIGHT
    };

    self.iniFile = undefined;
    self.saveIni = false;
    self.hasBorder = false;

    self.windowCreationProperties = { resizeable: false };
    self.center = false;
    self.setDefault = false;
    self.textOfs = 3;
};
ContactSheetUI.prototype.typename = "ContactSheetUI";

ContactSheetUI.CSII_WIDTH = 560;
ContactSheetUI.CSII_HEIGHT = 700;

//
// Not used at this time. Would be used for putting page numbers on sheets
//
ContactSheetUI.nextPageNumber = function (folder, prefix) {
    var st = ContactSheetUI.zeroPad(1, ContactSheetIIOptions.PAGE_PAD);
    var p = ContactSheetUI.getNextIndex(folder, prefix, st);
    return ContactSheetUI.zeroPad(Number(p), ContactSheetIIOptions.PAGE_PAD);
};


//
// Just returns 1 at this time. Would be used for determining the
// next file number if sheets were saved into a folder with previously
// generated sheets
//
ContactSheetUI.nextFileNumber = function (folder, format) {
    return 1;

    if (!len) {
        len = ContactSheetIIOptions.FILE_PAD;
    }
    return ContactSheetUI.getNextIndex(folder, format, 1);
};

//
// Not used. Needed when saving to sheets to files
//
ContactSheetUI.getNextIndex = function (folder, format, def) {
    var idx = 0;

    if (!(folder instanceof Folder)) {
        return def;
    }

    if (folder.alias) {
        folder = folder.resolve();
    }

    var ar = format.split('-');
    prefix = decodeURI(ar[0]) + '-';
    var rex = RegExp(prefix + "(\\d+)");

    var flist = folder.getFiles(rex);
    if (flist && flist.length) {
        for (var i = 0; i < flist.length; i++) {
            var file = flist[i];
            var m = file.name.match(/(\d+)\.[^\.]+$/);
            if (m) {
                var v = Number(m[1]);
                if (v > idx) {
                    idx = v;
                    pad = m[1].length;
                }
            }
        }

        idx++;
    }

    return idx;
};
ContactSheetUI.prototype.getNextIndex = ContactSheetUI.getNextIndex;

//
// Function: fieldErrorPrompt
// Description: opens an alert window if appropriate
// Input: an error message
// Return: false
//
ContactSheetUI.fieldErrorPrompt = function (msg) {
    // shold probably check for dialogMode
    if (app.dialogModes != DialogModes.NO) {
        alert(msg);
    }
    return false;
};

//
// Function: invalidValuePrompt
// Description: opens an alert window for a field with an invalid value
// Input: the name of a UI field
// Return: false
//
ContactSheetUI.invalidValuePrompt = function (nm) {
    var msg = ZStrings.InvalidValueMsg.sprintf(nm);
    return ContactSheetUI.fieldErrorPrompt(msg);
};

//
// Function: invalidNumberPrompt
// Description: opens an alert window for a field with an invalid number
// Input: the name of a UI field
// Return: false
//
ContactSheetUI.invalidNumberPrompt = function (nm) {
    var msg = ZStrings.InvalidNumberMsg.sprintf(nm);
    return ContactSheetUI.fieldErrorPrompt(msg);
};

//
// Function: numberOutOfRange
// Description: opens an alert window for a field with number out of range
// Input: the name of a UI field, the minimum and maximum values of the range
// Return: false
//
ContactSheetUI.numberOutOfRange = function (nm, min, max) {
    if (typeof min == "number") {
        min = "%.03f".sprintf(min);
    }
    if (typeof max == "number") {
        max = "%.03f".sprintf(max);
    }

    // XXX - we should really change the zstring db to %s...
    var fmt = ZStrings.NumberOutOfRangeMsg.replace(/%\.03f/g, "%s");
    var msg = fmt.sprintf(nm, psx.localizeNumber(min), psx.localizeNumber(max));

    return ContactSheetUI.fieldErrorPrompt(msg);
};

//
// Function: getNumber
// Description: get the number from a UI field. Errors get raised where needed
// Input:  s    - the string from the UI
//         type - the UnitValue type
//         nm   - the name of the UI field
//         min  - the minimum allowed px value for the field
//         min  - the maximum allowed px value for the field
//         base - the baseUnit appropriate for the field
// Return: undefined - if the number is invalid
//         number    - the value of the field as a number. If the original
//                     value was out of range, the minimum or maximum
//                     value will be returned
//
ContactSheetUI.getNumber = function (s, type, nm, min, max, base) {
    var n = toNumber(s);
    if (isNaN(n)) {
        ContactSheetUI.invalidNumberPrompt(nm);
        return undefined;
    }

    var unit = UnitValue(n, type);
    unit.baseUnit = base;

    var val = unit.as("px");

    if (val < min || val > max) {
        var un = UnitValue(max, "px");
        un.baseUnit = base;
        var v = un.as(type);
        max = toNumber(v.toFixed(3));
        if (max > v) {
            max -= 0.001;
        }
        un.value = min;
        v = un.as(type);
        min = toNumber(v.toFixed(3));
        if (min < v) {
            min += 0.001;
        }
        ContactSheetUI.numberOutOfRange(nm, min, max);
        n = (unit.value < min) ? min : max;
    }
    return n;
};

//
// Not used. This is an alternative to alert()
//
ContactSheetUI.prototype.popup = function (title, str, win, bnds) {
    if (!bnds) {
        bnds = [200, 200, 700, 750];
    }
    var w = new Window('dialog', title, bnds);
    var t = w.add('edittext', [10, 10,
        bnds[2] - bnds[0] - 10,
        bnds[3] - bnds[1] - 10],
        '', { multiline: true, readonly: true });
    t.text = str;
    if (win) {
        w.center(win);
    }
    w.show();
};


//
// Not used. Returns the contents of the log file for use
// in error alerts.
//
ContactSheetUI.prototype.getLogFileContents = function () {
    var file = new File(ContactSheetIIOptions.LOG_FILE);

    var str = "Log File: " + file.toUIString() + '\n';
    if (file.exists) {
        str += psx.readFromFile(file);
    } else {
        str += "Log file not found.";
    }

    return str;
};


//
// Function: zeroPad
// Description: pads a number with zeros on the left to a desired width
// Input:  num - the number
//         w - the total number of desired digits
// Return: the padded string
//
ContactSheetUI.zeroPad = function (num, w) {
    var str = num.toString();

    while (str.length < w) {
        str = "0" + str;
    }
    return str;
};


//
// Not used. Returns the number of pages and images to be generated
//
ContactSheetUI.prototype.getPageAndImageCounts = function () {
    var self = this;
    var opts = new ContactSheetIIOptions();
    var win = self.win;
    var appPnl = win.appPnl;

    Error.runtimeError(9500, ZStrings.UnknownInternalError);

    self.validateSourcePanel(appPnl.source, opts);

    if (!appPnl.layoutPanel) {
        return;
    }
    opts.rowCount = appPnl.layoutPanel.rows.value;
    opts.columnCount = appPnl.layoutPanel.cols.value;
    if (opts.folder) {
        opts.folder = new Folder(opts.folder);
    }

    var fileSets = ContactSheetII.getSourceImages(opts);
    var pages = fileSets.length;
    var images = 0;
    for (var i = 0; i < fileSets.length; i++) {
        var set = fileSets[i];
        images += set.length;
    }

    return { pages: pages, images: images };
};

//
// Function: createContactSheetII
// Description: create a contact sheet generator based on the UI settings
// Input: <none>
// Return: a ContactSheetII object
//
ContactSheetUI.prototype.createContactSheetII = function () {
    return new ContactSheetII(this.settings);
};

//
// Function: convertValueToUnit
// Description: convert a value from one unit to another unit
// Input: value - the value which has to be converted
//        fromUnit - the current unit of the value
//        toUnit - the result unit
//        baseUnit - the resolution base
// Return: value converted to toUnit
//
ContactSheetUI.convertValueToUnit = function (value, fromUnit, toUnit, baseUnit) {
    var fromValue = UnitValue(value, fromUnit);
    if (!baseUnit) {
        baseUnit = UnitValue(1 / 72, "in");
    }
    fromValue.baseUnit = baseUnit;
    var result = fromValue.as(toUnit);
    return result;
};

//============================== END OF UI ================================

//
// ContactSheetII - the contact sheet generator
// Input: CSII XML settings
//
ContactSheetII = function (settings) {
    var self = this;

    self.settings = settings;

    self.now = new Date();
    self.dateStr = '';
    self.imageCount = 0;
    self.errorCount = 0;
    self.skipImageErrors = false;
};
ContactSheetII.prototype.typename = "ContactSheetII";

//
// Function: isCompatible
// Description: determines whether the version of PS is valid for CSII
// Input: <none>
// Return: true if compatible, false if not
//
ContactSheetII.isCompatible = function () {
    return toNumber(app.version.match(/^\d+/)[0]) >= CSII.REQUIRED_PS_VERSION;
};

//
// Function: toDescriptor
// Description: creates a descriptor for an XML settings object
// Input:  an XML settings object
// Return: an ActionDescriptor containing the XML settings as a string
//         as one of the properties
//
ContactSheetII.toDescriptor = function (xml) {
    var desc = new ActionDescriptor();
    var str = xml.toXMLString();

    desc.putString(CSII.kCSIISettings, str);

    return desc;
};

//
// Function: fromDescriptor
// Description: creates an XML settings object from a descriptor
// Input:  an ActionDescriptor in either legacy format or from
//         the new XML settings format
// Return: an XML settings object
//
ContactSheetII.fromDescriptor = function (desc) {

    // see if its in the new XML format
    if (desc.hasKey(CSII.kCSIISettings)) {
        var str = desc.getString(CSII.kCSIISettings);
        return new XML(str);
    }

    // if there is no Units, assume the descriptor
    // is incomplete or invalid and bail out

    if (!desc.hasKey(Keys.Units)) {
        return undefined;
    }

    // It may be an old style legacy descriptor
    var settings = ContactSheetUI.getDefaultOptions();

    //======= Source Panel ================

    var source = settings.source;
    var inputSource = (desc.hasKey(Keys.InputSource) ?
        desc.getEnumerationValue(Keys.InputSource) : Enums.Folder);

    switch (inputSource) {
        case Enums.CurrentDocs:
            source.@imageSource = ZStrings.CurrentDocs;
            break;

        case Enums.Folder:
            source.@imageSource = ZStrings.Folder;
            try {
                source.@path = desc.getPath(Keys.InputLocation);

            } catch (e) {
                var msg = ZStrings.BadRecordingFmt.sprintf(ZStrings.BadFolder);

                if (app.playbackDisplayDialogs == DialogModes.ALL) {
                    delete source.@path;
                    alert(msg);
                } else {
                    Error.runtimeError(9006, msg);
                }
            }
            source.@includeSubfolders = desc.getBoolean(Keys.IncludeSubDir);
            source.@groupImages = false;
            break;

        case Enums.FileBrowser:
            source.@imageSource = ZStrings.Bridge;
            break;

        case Enums.Files:
            source.@imageSource = ZStrings.Files;
            break;

        default:
            Error.runtimeError(9006, ZStrings.BadRecordingFmt.sprintf(ZStrings.BadSource));
    }

    //============ Document Panel ==============

    var document = settings.document;
    var units = desc.getEnumerationValue(Keys.Units);
    if (units == Enums.UnitInches) {
        document.@units = ZStrings.Inches;
    } else if (units == Enums.UnitCentimeter) {
        document.@units = ZStrings.Centimeter;
    } else if (units == Enums.UnitPixels) {
        document.@units = ZStrings.Pixels;
    } else {
        Error.runtimeError(9006, ZStrings.BadRecordingFmt.sprintf(ZStrings.BadUnits));
    }

    var res = desc.getUnitDoubleValue(Keys.Resolution);

    var resUnits = desc.getEnumerationValue(Keys.ResolutionUnits);
    if (resUnits == Enums.PixelsPerInch) {
        document.@resUnits = ZStrings.PixelsPerInch;
    } else if (resUnits == Enums.PixelsPerCentimeter) {
        document.@resUnits = ZStrings.PixelsPerCM;
    } else {
        Error.runtimeError(9006,
            ZStrings.BadRecordingFmt.sprintf(ZStrings.BadResolution));
    }

    // res is stored as pixels per inch

    if (document.@resUnits == ZStrings.PixelsPerCM) {
        document.@resolution = res / 2.54;
    } else {
        document.@resolution = res;
    }

    var h = desc.getUnitDoubleValue(Keys.Height) * res / 72;
    var w = desc.getUnitDoubleValue(Keys.Width) * res / 72;

    var baseUnit = new UnitValue(1 / res, "in");
    var un = new UnitValue(w, "px");
    un.baseUnit = baseUnit;

    document.@width = un.as(document.@units);

    un.value = h;
    document.@height = un.as(document.@units);


    var mode = desc.getEnumerationValue(Keys.Mode);
    switch (mode) {
        case Enums.CMYKMode:
            document.@mode = ZStrings.CMYKMode;
            break;

        case Enums.LabMode:
            document.@mode = ZStrings.LabMode;
            break;

        case Enums.GrayscaleMode:
            document.@mode = ZStrings.GrayscaleMode;
            break;

        case Enums.RGBMode:
            document.@mode = ZStrings.RGBMode;
            break;

        default:
            Error.runtimeError(9006,
                ZStrings.BadRecordingFmt.sprintf(ZStrings.BadColorMode));
    }

    // use defaults...
    document.@bitDepth = ZStrings.BitDepth8;
    document.@colorProfile = ZStrs.ProfileSRGB;

    document.@flattenLayers = desc.getBoolean(Keys.FlattenText);


    // ======== Thumbnail Panel ===========

    var thumbnail = settings.thumbnail;
    var rowOrdered = desc.getBoolean(Keys.RowOrdered);
    if (rowOrdered) {
        thumbnail.@place = ZStrings.AcrossFirst;
    } else {
        thumbnail.@place = ZStrings.DownFirst;
    }

    thumbnail.@cols = desc.getInteger(Keys.Columns);
    thumbnail.@rows = desc.getInteger(Keys.Rows);
    thumbnail.@bestFit = (desc.hasKey(Keys.RotateForBestFit) &&
        desc.getBoolean(Keys.RotateForBestFit));

    var uas = (desc.hasKey(Keys.UseAutoSpacing) &&
        desc.getBoolean(Keys.UseAutoSpacing));
    thumbnail.@useAutospacing = uas;

    if (!uas) {
        var h = desc.getUnitDoubleValue(Keys.HorizontalSpacing) * res / 72;
        var v = desc.getUnitDoubleValue(Keys.VerticalSpacing) * res / 72;

        var baseUnit = new UnitValue(1 / res, "in");
        var un = new UnitValue(h, "px");
        un.baseUnit = baseUnit;

        thumbnail.@horz = un.as(document.@units);

        un.value = v;
        thumbnail.@vert = un.as(document.@units);
    }


    // ======== Caption Panel ===========

    var caption = settings.caption;

    var cap = desc.hasKey(Keys.IncludeName) && desc.getBoolean(Keys.IncludeName);
    caption.@enabled = cap;

    var font = undefined;
    var fkind = desc.getEnumerationValue(Keys.Font);
    if (fkind == Enums.Serif) {
        font = (isWindows() ? ZStrings.WinSerif :
            ContactSheetII.fixMacFont(ZStrings.MacSerif));

    } else if (fkind == Enums.SansSerif) {
        font = (isWindows() ? ZStrings.WinSansSerif :
            ContactSheetII.fixMacFont(ZStrings.MacSansSerif));

    } else if (fkind == Enums.Courier) {
        font = (isWindows() ? ZStrings.WinCourier :
            ContactSheetII.fixMacFont(ZStrings.MacCourier));

    } else {
        Error.runtimeError(9006, ZStrings.BadLegacyFont);
    }

    caption.font.@name = toFont(font);
    caption.font.@size = desc.getInteger(Keys.FontSize);

    return settings;
};


//
// Function: fixMacFont
// Description: The default fonts for the Mac changed but the ZString weren't
// Input: an original Mac font name
// Return: the correct "new" Mac font name
//
ContactSheetII.fixMacFont = function (font) {
    switch (font) {
        case "Courier": font = "Myriad Pro"; break;
        case "Helvetica": font = "Minion Pro"; break;
        case "Times": font = "Lucida Grande"; break;
    }
    return font;
};

// This is a hack to reset the default fontName and fontFaceName on the mac
function _fixDefaultMacFont() {
    if (!isMac()) {
        return;
    }

    CSIIDefaults.fontName = ContactSheetII.fixMacFont(ZStrings.MacSansSerif);
    CSIIDefaults.fontFaceName = ContactSheetII.fixMacFont(ZStrings.MacSansSerif);
};
_fixDefaultMacFont();


//
// Function: process
// Description: Does setup, error handling, and timing for the sheet generator
// Input: ContactSheetIIOptions
// Return: ContactSheetIIOptions
//
ContactSheetII.prototype.process = function (opts) {
    var self = this;

    function handleError(e, pre) {
        LogFile.logException(e);

        var str = (pre || ZStrings.ErrorInProcessing) + e.toString();

        str += ("\n\n" + ZStrings.LogFileReference + "\n    " +
            decodeURI(ContactSheetIIOptions.LOG_FILE));

        return str;
    }

    var timer = new Timer();
    timer.start();

    self.imageProcessingTimer = new Timer();

    if (ContactSheetIIOptions.LOG_ENABLED) {
        LogFile.setFilename(ContactSheetIIOptions.LOG_FILE, "UTF8");
    }

    LogFile.write("Version: " + CSII.VERSION);
    LogFile.write("Revision: $Revision: 1.9 $");
    LogFile.write("App: " + app.name);
    LogFile.write("App Version: " + app.version);
    LogFile.write("OS: " + $.os);

    LogFile.write("Begin Processing");
    LogFile.write(psx.listProps(opts));

    try {
        // if this fails, it will only cause a problem for recording in an action
        // this is a non-fatal error
        self.descriptor = ContactSheetII.toDescriptor(self.settings);

    } catch (e) {
        var str = handleError(e, ZStrings.ActionRecordingError);
        self.descriptor = undefined;

        if (opts.forceRecording) {
            self.errorCount++;
        }
    }

    var ru = app.preferences.rulerUnits;
    app.preferences.rulerUnits = Units.PIXELS;
    var tu = app.preferences.typeUnits;
    app.preferences.typeUnits = TypeUnits.POINTS;

    try {
        self._processX(opts);

        // post processing
        LogFile.write("End Processing");

        timer.stop();

        var s = String.sprintf("%.3f seconds overall per image",
            1.0 * timer.elapsed / self.imageCount);
        LogFile.write(s);

        var tmr = self.imageProcessingTimer;
        if (!isNaN(tmr.per)) {
            s = String.sprintf("%.3f seconds processing time per image", tmr.per);
            LogFile.write(s);
        }

    } catch (e) {
        if (e && e.number != 8007) {  // if not User Cancelled Operation
            var str = handleError(e, ZStrings.ErrorInProcessing);

            alert(str);
        }

        self.descriptor = undefined;

    } finally {
        app.preferences.rulerUnits = ru;
        app.preferences.typeUnits = tu;
    }

    if (self.errorCount) {
        var str = ZStrings.ErrorsDetectedFmt.sprintf(self.errorCount);
        str += "\r" + ZStrings.LogFileReference;
        str += "\r" + decodeURI(ContactSheetIIOptions.LOG_FILE);

        alert(str);
    }

    return opts;
};

//
// Function: _processX
// Description: Initializes sheet generation, then calls processFiles
//              to generate the individual sheets
// Input: ContactSheetIIOptions
// Return: <none>
//
ContactSheetII.prototype._processX = function (opts) {
    var self = this;

    self.openDocMode = (opts.imageSource == ZStrings.CurrentDocs);

    opts.noUI = toBoolean(opts.noUI);

    var fileSets = ContactSheetII.getSourceImages(opts);

    if (fileSets.length == 0) {
        var msg = ZStrings.NoImageFiles;
        LogFile.write(msg);
        alert(msg);
        return;
    }
    self.fileSets = fileSets;

    app.purge(PurgeTarget.ALLCACHES);

    if (opts.resUnits == 'cm') {
        opts._resolution = opts.resolution;
        opts.resolution = opts.resolution * 2.54;
    }

    var sheet;
    opts.mode = ContactSheetIIOptions.toMode(opts.mode);

    opts.leftMargin = 0;
    opts.rightMargin = 0;
    opts.topMargin = 0;
    opts.bottomMargin = 0;

    opts._width = opts.docWidth;
    opts._height = opts.docHeight;

    if (opts.docUnits != 'pixels') {
        var res;

        if (opts.docUnits == 'inches') {
            res = opts.resolution;

        } else if (opts.docUnits == 'cm') {
            res = opts.resolution / 2.54;
        }

        opts.docWidth = Math.round(opts.docWidth * res);
        opts.docHeight = Math.round(opts.docHeight * res);

        // would need to adjust margins here if used...
    }

    var w = opts.docWidth;
    var h = opts.docHeight;
    var res = opts.resolution;

    // configure autospacing
    if (opts.useAutoSpacing) {
        var v = (w / opts.columnCount) * opts.autospaceFactor / 100;
        opts.horizontal = Math.floor(v);
        //     v = (h /opts.rowCount) * opts.autospaceFactor / 100;
        //     opts.vertical = Math.floor(v);

        opts.vertical = opts.horizontal;

    } else {
        if (opts.spacingUnits != "px") {
            Error.runtimeError(9200, "Bad spacingUnits value");
        }
    }

    // these 'frame' dimensions describe the size of the thumbnail/caption
    // area in a non-template-based sheet (would need to 
    opts.frameWidth = opts.docWidth - (opts.leftMargin + opts.rightMargin);
    opts.frameHeight = opts.docHeight - (opts.topMargin + opts.bottomMargin);

    // font size is already in points
    opts.fontSizePT = Math.floor(opts.fontSize);

    if (opts.caption) {
        if (opts.captionFilename) {
            opts.captionFormat = (opts.noExtensions ? "%f" : "%f.%e");
        }
    }

    //   if (opts.nextPage) {
    //     var start = ContactSheetUI.nextPageNumber(opts.outputFolder,
    //                                               opts.fileNameFormat);
    //     opts._pageStart = Number(start);
    //     opts._pagePad = start.length;
    //   }

    //   if (opts.nextFile) {
    //     var start = ContactSheetUI.nextFileNumber(opts.outputFolder,
    //                                               opts.fileNameFormat);
    //     opts._outputStart = Number(start);
    //     opts._outputPad = start.length;
    //   }

    self.currentPageNumber = opts._pageStart;
    self.currentFileIndex = opts._outputStart;

    LogFile.write("Options:\n" + psx.listProps(opts));


    for (var i = 0; i < fileSets.length; i++) {
        var set = fileSets[i];

        if (set.length > 0) {
            opts.files = set;

            var rc = self.processFiles(opts);

            if (!rc) {
                break;
            }
        }
    }
};


//
// Function: processFiles
// Description: calls contactSheetX to create a sheet then do any
//              additional processing on the sheet (e.g. flatten)
// Input: ContactSheetIIOptions
// Return: true if successful, false if not
//
ContactSheetII.prototype.processFiles = function (opts) {
    var self = this;

    var csLayer;        // the Contact Sheet layer, if there is one
    var csdoc;
    var fileCount = opts.files.length;

    // create and populate a sheet
    csdoc = self.contactSheetX(opts);

    if (!csdoc) {
        return false;
    }

    // it's not a template-based document

    if (opts.flatten) {
        // The legacy plugin has slightly different behavior here.
        // All of the layers are merged together except the background.
        // and the name of the merged layer is the name of the last
        // image added
        if (CSII.USE_LEGACY_FLATTEN && psx.hasBackground(csdoc)) {
            var name = csdoc.layers[csdoc.layers.length - 2].name;
            csdoc.backgroundLayer.visible = false;
            psx.mergeVisible();
            csdoc.backgroundLayer.visible = true;
            csdoc.activeLayer.name = name;

        } else {
            csdoc.flatten();
            csdoc.activeLayer.name = CSII.CS_LAYER_NAME;
        }
    }

    if (opts.saveSheet) {
        self.saveDocument(csdoc, opts);
    }
    if (!opts.keepOpen) {
        csdoc.close(SaveOptions.DONOTSAVECHANGES);
    }

    self.currentPageNumber++;
    self.currentFileIndex++;

    return true;
};

//
// Function: createSheet
// Description: create a new empty sheet document with the desired properties
// Input: ContactSheetIIOptions
// Return: a Document
//
ContactSheetII.prototype.createSheet = function (opts) {
    var self = this;
    var doc;

    var name = self.getSheetName(opts);

    var bpc = BitsPerChannelType.EIGHT;
    if (opts.bitDepth && opts.bitDepth.contains("16")) {
        bpc = BitsPerChannelType.SIXTEEN;
    }

    try {
        doc = app.documents.add(opts.docWidth,
            opts.docHeight,
            opts.resolution,
            name,
            opts.mode,
            DocumentFill.WHITE,
            1,
            bpc);
    } catch (e) {
        var msg;
        if (e.number == 8007) {
            msg = ZStrs.UserCancelled;

        } else {
            msg = (ZStrings.CreateFailure + "\r\r\"" + e.toString() + "\"");
            Error.runtimeError(9001, msg);
        }
    }

    self.convertProfile(doc, opts.colorProfile);

    // need to see if the mode got changed...
    if (opts.mode.toString() != ("New" + doc.mode.toString())) {
        var str = opts.mode.toString().replace('NewDocument', 'Change');
        var mode = eval(str);
        doc.changeMode(mode);
    }

    return doc;
};

//
// Function: convertProfile
// Description: convert a document to the desired profile. This requires
//              some unexpected subtlety
// Input: doc     - Document
//        profile - the name of the desired profile
// Return: <none>
//
ContactSheetII.prototype.convertProfile = function (doc, profile) {
    var self = this;
    var convert = true;

    if (profile == ZStrs.ProfileLab && doc.mode == DocumentMode.LAB) {
        return;
    }

    if (profile == ZStrs.ProfileWorkingGray && doc.mode == DocumentMode.GRAYSCALE) {
        return;
    }

    if (profile == ZStrs.ProfileWorkingCMYK && doc.mode == DocumentMode.CMYK) {
        return;
    }

    if (profile == ZStrs.ProfileWorkingRGB && doc.mode == DocumentMode.RGB) {
        return;
    }

    if (doc.colorProfileType != ColorProfile.NONE) {
        convert = doc.colorProfileName != profile;
    }

    if (convert) {
        LogFile.write("Convert color profile for " + decodeURI(doc.name) +
            " to " + profile);
        try {
            psx.convertProfile(doc, profile);

        } catch (e) {
            // convertProfile throws an 8007 if it doesn't know the profile.
            // Remap it to something else so it gets caught by the top-level
            // error handling code
            if (e.number == 8007) {
                e.number = 9100;
                e.message = ZStrings.ProfileConversionError;
            }
            Error.runtimeError(e.number, e.message);
        }
    }
};

//
// Function: getSheetName
// Description: create a name for a new contact sheet
// Input: ContactSheetIIOptions
// Return: a sheet name
//
ContactSheetII.prototype.getSheetName = function (opts) {
    var self = this;
    var name;
    var base = '';

    var base = opts.fileNameFormat.sprintf(self.currentFileIndex);

    if (opts.saveSheet) {
        name = base + '.' + opts.fileSaveType;

    } else {
        name = base;
    }

    // base = (opts.outputPrefix +
    //         self.zeroPad(self.currentFileIndex, opts._outputPad));

    return name;
};

//
// Not used in this release.
//
ContactSheetII.prototype.saveDocument = function (doc, opts) {
    var self = this;

    Error.rutimeError(9500, ZStrings.UnknownInternalError);

    // save as PSD files only

    if (!opts.saveSheet) {
        LogFile.write("Contact Sheet not saved #" + self.currentPageNumber);
        return;
    }

    var profileConverted = false;

    var saveOpts = self.getSaveOpts(opts);

    var name = self.getSheetName(opts);

    if (opts._formatPanel) {
        var fname = (opts.outputFolder + '/' + name);

        var file = new File(fname);

        if (opts.fileSaveType == 'jpg') {
            var bpc = doc.bitsPerChannel;
            if (bpc != BitsPerChannelType.ONE && bpc != BitsPerChannelType.EIGHT) {
                doc.bitsPerChannel = BitsPerChannelType.EIGHT;
            }
        }

        if (saveOpts._flatten) {
            doc.flatten();
        }

        if (saveOpts._convertToIndexed) {
            if (doc.mode != DocumentMode.INDEXEDCOLOR) {
                if (doc.mode != DocumentMode.RGB) {
                    doc.changeMode(ChangeMode.RGB);
                }
                var cnvtOpts = new IndexedConversionOptions();
                doc.changeMode(ChangeMode.INDEXEDCOLOR, cnvtOpts);
            }
        }

        if (saveOpts._convertToSRGB) {
            try {
                LogFile.write("Converting to sRGB");

                self.convertProfile(doc, ZStrs.ProfileSRGB);
                profileConverted = true;

            } catch (e) {
            }
        }

    } else {
        var fname = opts.outputFolder + '/' + base + '.' + opts.outputFormat;

        var file = new File(fname);

        if (opts.outputFormat == 'jpg') {
            var bpc = doc.bitsPerChannel;
            if (bpc != BitsPerChannelType.ONE && bpc != BitsPerChannelType.EIGHT) {
                doc.bitsPerChannel = BitsPerChannelType.EIGHT;
            }
        }
    }

    LogFile.write("Saving sheet: " + file.toUIString());
    doc.saveAs(file, saveOpts, false);

    LogFile.write("Contact Sheet saved " + file.toUIString());
};


//
// Function: zeroPad
// Description: pads a number with zeros on the left to a desired width
// Input:  num - the number
//         w - the total number of desired digits
// Return: the padded string
//
ContactSheetII.prototype.zeroPad = function (num, w) {
    var str = num.toString();

    while (str.length < w) {
        str = "0" + str;
    }
    return str;
};

//
// Function: getSourceImages
// Description: get the source images/files collected into page sets
// Input: ContactSheetIIOptions
// Return: an array of page sets which are arrays of images/files
//
ContactSheetII.getSourceImages = function (opts) {
    var fileSets;

    if (opts.imageSource == ZStrings.Folder && opts.folder &&
        !(opts.folder instanceof Folder)) {
        opts.folder = new Folder(opts.folder);
    }

    if (opts.files && opts.files.length > 0) {
        fileSets = ContactSheetII.getFileSetsFromFiles(opts, opts.files);

    } else if (opts.imageSource == ZStrings.CurrentDocs) {
        fileSets = ContactSheetII.getImageSets(opts);

    } else {
        fileSets = ContactSheetII.getFileSets(opts);
    }
    return fileSets;
};

//
// Function: getImageSets
// Description: get the open images collected into page sets
// Input: ContactSheetIIOptions
// Return: an array of page sets which are arrays of open images
//
ContactSheetII.getImageSets = function (opts) {
    var sets = [];
    var setSize = opts.rowCount * opts.columnCount;

    var docs = app.documents;

    var images = [];

    for (var i = 0; i < docs.length; i++) {
        images.push(docs[i]);
    }

    //   ContactSheetII.caseInsensitiveFileSort(images);
    docs = images;

    var currentSet = [];

    for (var i = 0; i < docs.length; i++) {
        currentSet.push(docs[i]);
        if (currentSet.length == setSize) {
            sets.push(currentSet);
            currentSet = [];
        }
    }

    if (currentSet.length != 0) {
        sets.push(currentSet);
    }

    return sets;
};


//
// Function: getFileSetsFromFiles
// Description: get the selected files collected into page sets
// Input: opts - ContactSheetIIOptions
//        files - image files
// Return: an array of page sets which are arrays of files
//
ContactSheetII.getFileSetsFromFiles = function (opts, files) {
    var sets = [];
    var setSize = opts.rowCount * opts.columnCount;
    var span = opts.spanFolders;

    var currentSet = [];
    var folderStr = files[0].parent.absoluteURI;

    //   ContactSheetII.caseInsensitiveFileSort(files);

    while (files.length > 0) {
        var file = files.shift();
        if (file.parent.absoluteURI != folderStr && !span) {
            folder = file.parent;
            sets.push(currentSet);
            currentSet = [];
        }
        currentSet.push(file);
        if (currentSet.length == setSize) {
            sets.push(currentSet);
            currentSet = [];
        }
    }
    if (currentSet.length != 0) {
        sets.push(currentSet);
    }

    return sets;
};


//
// Function: caseInsensitiveFileSort
// Description: sorts an array of files case insensitive
// Input: list - an array of files
// Return: a sorted array of files
//
ContactSheetII.caseInsensitiveFileSort = function (list) {
    function ciCmp(a, b) {
        return app.compareWithNumbers(a.name, b.name)
    }
    return list.sort(ciCmp);
};

//
// Function: caseInsensitiveSort
// Description: sorts an array of strings case insensitive
// Input: list - an array of strings
// Return: a sorted array of strings
//
ContactSheetII.caseInsensitiveSort = function (list) {
    function ciCmp(a, b) {
        return app.compareWithNumbers(a, b)
    }
    return list.sort(ciCmp);
};

//
// Function: getFileSets
// Description: get the selected files collected into page sets
//              This function is called when source is Bridge or Folder
// Input: opts - ContactSheetIIOptions
// Return: an array of page sets which are arrays of files
//
ContactSheetII.getFileSets = function (opts) {
    var sets = [];
    var setSize = opts.rowCount * opts.columnCount;
    var span = opts.spanFolders;

    var currentSet = [];

    function collectSets(f) {
        var images = f.images;
        var ss = sets;
        var cs = currentSet;

        while (images.length) {
            currentSet.push(images.shift());
            if (currentSet.length == setSize) {
                sets.push(currentSet);
                currentSet = [];
            }
        }
        if (currentSet.length != 0 && !span) {
            sets.push(currentSet);
            currentSet = [];
        }
        var folders = f.subfolders;
        while (folders.length) {
            collectSets(folders.shift());
        }
    };

    // 
    if (opts.imageSource == ZStrings.Folder) {
        var tree = ContactSheetII.getDirectoryTree(opts);
        if (tree == undefined) {
            var msg = ZStrings.BadRecordingFmt.sprintf(ZStrings.BadFolder);
            Error.runtimeError(9006, msg);
        }

        collectSets(tree);

        if (currentSet.length != 0) {
            sets.push(currentSet);
        }

    } else if (opts.imageSource == ZStrings.Bridge) {
        if (!BridgeTalk.isRunning("bridge")) {
            Error.runtimeError(9006, ZStrings.BridgeFilesNotSelected);
        }

        var files = ContactSheetUI.getImageFilesFromBridge();
        if (!files || files.length == 0) {
            Error.runtimeError(9006, ZStrings.BridgeFilesNotSelected);
        }

        sets = ContactSheetII.getFileSetsFromFiles(opts, files);

    } else {
        // Should not reach this block
        Error.runtimeError(9500, ZStrings.UnknownInternalError);
    }

    return sets;
};

//
// Function: getDirectoryTree
// Description: Called when source is Folder to collect required files.
// Input: opts - ContactSheetIIOptions
// Return: A 'tree' object containing a reference to the root folder,
//         the image files in the folder, and an array of 'tree' objects
//         for subfolders (if indicated by opts.recurse)
//
ContactSheetII.getDirectoryTree = function (opts) {

    function getTree(f, recurse) {
        var tree = {
            folder: f,
            images: [],
            subfolders: [],
        };

        var files = f.getFiles();
        var flist = [];
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            if (psx.isValidImageFile(file) || file instanceof Folder) {
                flist.push(file);
            }
        }

        ContactSheetII.caseInsensitiveFileSort(flist);

        for (var i = 0; i < flist.length; i++) {
            var fl = flist[i];

            if (fl instanceof Folder && recurse) {
                var sub = getTree(fl, true);
                tree.subfolders.push(sub);

            } else if (psx.isValidImageFile(fl) && !fl.hidden) {
                tree.images.push(fl);
            }
        }

        return tree;
    }

    if (opts.imageSource == ZStrings.Folder && opts.folder &&
        opts.folder instanceof Folder) {
        var tree = getTree(opts.folder, opts.recurse);
        return tree;
    }

    return undefined;
};


//
// Function: computeBounds
// Description: Given the cell width and height, set properties in csOpts
//              that determine the bounds the image and (optional) caption
//              within a contact sheet cell
// Input:  csOpts - ContactSheetIIOptions
//         cellW - cell width
//         cellH - cell height
// Return: <none>
//
ContactSheetII.prototype.computeBounds = function (csOpts, cellW, cellH) {
    var self = this;

    // compute the cell bounds
    csOpts.cellBnds = [0, 0, cellW, cellH];

    csOpts.landscape = (cellW > cellH);

    // compute the basic image bounds with spacing
    var imageW = cellW - (2 * csOpts.horizontal);
    var imageH = cellH - (2 * csOpts.vertical);

    csOpts.imageBnds = [csOpts.horizontal, csOpts.vertical,
    imageW + csOpts.horizontal, imageH + csOpts.vertical];

    var capHeight = 0;
    var overlay = false;

    var caption = csOpts.caption;

    if (caption) {
        var heightV = UnitValue(csOpts.fontSizePT, "pt");
        heightV.baseUnit = UnitValue(1 / csOpts.resolution, "in");
        capHeight = heightV.as('px');
        imageH -= Math.round(capHeight + csOpts.vertical / 2);
    }

    csOpts.imageBnds[2] = csOpts.imageBnds[0] + imageW;

    if (!caption) {
        csOpts.imageBnds[3] = csOpts.imageBnds[1] + imageH;
        csOpts.captionBnds = [0, 0, 0, 0];

    } else {
        csOpts.imageBnds[3] = csOpts.imageBnds[1] + imageH;

        // compute the bounds for the caption
        var yy;
        yy = Math.round(csOpts.imageBnds[3] + csOpts.vertical / 2);

        csOpts.captionBnds = [0, yy, cellW, yy + capHeight];
    }
};

//
// Function: offsetBounds
// Description: translate the rectangle defined by bnds by x,y
// Input:  bnds - the rectangle to be translated
//         x,y  - to amount translate
// Return: a translated copy of bnds
//
ContactSheetII.prototype.offsetBounds = function (bnds, x, y) {
    return [bnds[0] + x, bnds[1] + y, bnds[2] + x, bnds[3] + y];
};


//
// Function: contactSheetX
// Description: Creates a new sheet document and populates it with cells
// Input:  csOpts - ContactSheetIIOptions
// Return: the newly created and populated contact sheet
//
ContactSheetII.prototype.contactSheetX = function (csOpts) {
    var self = this;

    LogFile.write("Creating sheet " + self.currentPageNumber + " with files:\n" +
        csOpts.files.toString().replace(/,/g, '\n'));

    var xbase = 0;
    var ybase = 0;
    var doc;

    doc = self.createSheet(csOpts);

    var res = doc.resolution;

    var cellW = Math.floor(csOpts.frameWidth / csOpts.columnCount);
    var cellH = Math.floor(csOpts.frameHeight / csOpts.rowCount);

    self.computeBounds(csOpts, cellW, cellH);

    var row = 0;
    var col = 0;

    var okay = true;
    var isDone = false;

    if (csOpts.acrossFirst) {
        var useGuides = false; // XXX true for debugging

        for (var i = 0; i < csOpts.rowCount && okay && !isDone; i++) {
            var yofs = i * cellH + csOpts.topMargin + ybase;

            if (useGuides) {
                doc.guides.add(Direction.HORIZONTAL, UnitValue(yofs, "px"));
                doc.guides.add(Direction.HORIZONTAL, UnitValue(yofs + cellH, "px"));
            }

            for (var j = 0; j < csOpts.columnCount && okay && !isDone; j++) {
                var xofs = j * cellW + csOpts.leftMargin + xbase;

                if (useGuides) {
                    doc.guides.add(Direction.VERTICAL, UnitValue(yofs, "px"));
                    doc.guides.add(Direction.VERTICAL, UnitValue(yofs + cellH, "px"));
                }

                if (self.progressPalette && self.progressPalette.isDone) {
                    LogFile.write(ZStrings.UserCancelledProcessing);
                    okay = false;
                    break;
                }

                isDone = !self.createCell(doc, csOpts, xofs, yofs);
            }
        }
    } else {
        for (var j = 0; j < csOpts.columnCount && okay && !isDone; j++) {
            var xofs = j * cellW + csOpts.leftMargin + xbase;

            for (var i = 0; i < csOpts.rowCount && okay && !isDone; i++) {
                var yofs = i * cellH + csOpts.topMargin + ybase;

                if (self.progressPalette && self.progressPalette.isDone) {
                    LogFile.write(ZStrings.UserCancelledProcessing);
                    okay = false;
                    break;
                }

                isDone = !self.createCell(doc, csOpts, xofs, yofs);
            }
        }
    }

    if (okay) {
        LogFile.write("Sheet creation complete");

        if (csOpts.alternateMargins) {
            var m = csOpts.leftMargin;
            csOpts.leftMargin = csOpts.rightMargin;
            csOpts.rightMargin = m;
        }

    } else {
        doc = undefined;
    }

    return doc;
};

//
// Function: createCellHP
// Description: Creates a new cell (thumbnail and caption) in a sheet
//              using the next available file. This file is inserted
//              by either Place or ViewlessDocument methods
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
///        x,y - the location of the cell
// Return: true if successful, false if there are no more images
//
ContactSheetII.prototype.createCellHP = function (doc, csOpts, x, y) {
    var self = this;

    var bnds = self.offsetBounds(csOpts.imageBnds, x, y);

    var rc = false;
    while (!rc) {
        try {
            var file = csOpts.files.shift();

            if (!file) {
                return false;  // we're done
            }

            LogFile.write("Processing: " + file.toUIString());

            self.imageProcessingTimer.start();
            rc = self.insertImageHP(doc, csOpts, bnds, file);
        } catch (e) {
            if (e.number != CSII.kFileOpenError) {
                // Error.runtimeError(ZStrings.ErrorInProcessing);
            }
        }
    }

    self.imageProcessingTimer.stop();

    if (csOpts.caption) {
        var bnds = self.offsetBounds(csOpts.captionBnds, x, y);

        self.insertCaption(doc, csOpts, bnds, file);
    }

    return true;
};


//
// Function: createCell
// Description: Creates a new cell (thumbnail and caption) in a sheet
//              using the next available image. If the image is a file,
//              createCellHP is called. If not, the image is an open
//              document and is ultimately copy/pasted into the sheet
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
///        x,y - the location of the cell
// Return: true if successful, false if there are no more images
//
ContactSheetII.prototype.createCell = function (doc, csOpts, x, y) {
    var self = this;

    if (csOpts.files.length == 0) {
        return false;
    }

    if (csOpts.files[0] instanceof File) {
        return self.createCellHP(doc, csOpts, x, y);
    }

    var image = undefined;
    var bnds = self.offsetBounds(csOpts.imageBnds, x, y);

    var histState;

    while (image == undefined) {
        try {
            var file = csOpts.files.shift();

            if (!file) {
                return false;  // we're done
            }

            if (file.typename == "Document") {
                LogFile.write("Processing: " + file.name);

                image = file;
                file = psx.getDocumentName(image);
                app.activeDocument = image;

                image = image.duplicate(image.name, true);
                // try { psx.mergeVisible(image); } catch (e) {}

            } else {
                LogFile.write("Processing: " + file.toUIString());
                image = self.openImage(file);
            }

            self.imageProcessingTimer.start();

            if (image.colorProfileType != ColorProfile.NONE) {
                if (csOpts.colorProfile != image.colorProfileName) {
                    if (image.bitDepth != doc.bitDepth) {
                        try { image.bitDepth = doc.bitDepth; }
                        catch (e) { }
                        try { self.convertProfile(image, csOpts.colorProfile); }
                        catch (e) { }
                    }
                }
            }
        } catch (e) {
            if (e.number == 8007) { // User cancelled the operation
                Error.runtimeError(8007);
            }
            var msg = ('Image file not placed: ' + file + ' - ' + e.message +
                '@' + e.line);
            LogFile.write(msg);
            LogFile.logException(e);
            self.errorCount++;
        }
    }

    self.insertImage(doc, csOpts, bnds, image);

    self.imageProcessingTimer.stop();

    if (csOpts.caption) {
        var bnds = self.offsetBounds(csOpts.captionBnds, x, y);

        self.insertCaption(doc, csOpts, bnds, file, image);
    }

    app.activeDocument = image;
    image.close(SaveOptions.DONOTSAVECHANGES);
    app.activeDocument = doc;

    return true;
};

//
// Function: insertImage
// Description: Inserts an image into a sheet using the specified bounds
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
//         bnds - the rectangle into which the image is to be inserted
//         image - the open image to be inserted
// Return: the new image layer
//
ContactSheetII.prototype.insertImage = function (doc, csOpts, bnds, image) {
    var self = this;

    self.imageCount++;

    //   LogFile.write("Inserting image " + image.name + " at " + bnds);

    app.activeDocument = doc;
    var layer = doc.artLayers.add();

    // XXX app.activeDocument = image;
    var lname = File(image.name).strf("%f");

    app.activeDocument = doc;
    layer.name = lname;

    psx.selectBounds(doc, bnds, SelectionType.REPLACE, 0.0, false);

    var fit = !csOpts.autoFill;

    layer = self.insertImageIntoSelection(doc, csOpts, layer, image, fit);

    app.activeDocument = doc;
    doc.selection.deselect();

    self.alignThumbnail(doc, csOpts, bnds, layer);

    var style = csOpts.thumbStyle;
    if (style && style != CSII.NO_THUMB_STYLE) {
        layer.applyStyle(style);
    }

    if (csOpts.accumulateKeywords) {
        var iptc = new IPTC(doc);
        iptc.addKeywords(image.info.keywords);
    }

    return layer;
};


//
// Function: insertCaption
// Description: Inserts a caption for a thumbnail on a sheet. For performance
//              reasons, the first text layer created is used as a template
//              for subsequent captions
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
//         bnds - the rectangle into which the caption is to be inserted
//         file - the file used for the caption
//         image - the open image to be inserted
// Return: the text layer containing the caption
//
ContactSheetII.prototype.insertCaption = function (doc, csOpts, bnds, file,
    image) {
    var self = this;

    if (file == undefined) {
        file = File(image.name);
    }
    var caption = decodeURI(file.name);

    if (csOpts.captionFilename) {
        if (csOpts.noExtensions) {
            caption = file.strf("%f");
        }
    } else {
        caption = '';
    }

    //   LogFile.write("Inserting caption " + caption + " at " + bnds);

    app.activeDocument = doc;

    if (!self.cacheLayers) {
        self.cacheLayers = {};
    }

    var layer = self.cacheLayers[doc.name];
    var titem;

    if (!layer) {
        layer = doc.artLayers.add();
        doc.activeLayer = layer;
        self.cacheLayers[doc.name] = layer;
        layer.kind = LayerKind.TEXT;
        titem = layer.textItem;

        if (!csOpts._fontColor) {
            csOpts._fontColor = psx.colorFromString(csOpts.fontColor);
        }
        titem.color = csOpts._fontColor;
        titem.size = csOpts.fontSizePT;
        titem.font = csOpts.font;
        titem.kind = TextType.PARAGRAPHTEXT;  // XXX ???

        if (csOpts.autoscaleCaptions) {
            titem.minimumGlyphScaling = 75;
            titem.desiredGlyphScaling = 100;
        }

        titem.justification = Justification.CENTERJUSTIFIED;

        titem.width = bnds[2] - bnds[0];

        var style = csOpts.captionStyle;
        if (style && style != CSII.NO_CAPTION_STYLE) {
            layer.applyStyle(style);
        }

    } else {
        var imageLayer = doc.activeLayer;
        layer = layer.duplicate(imageLayer, ElementPlacement.PLACEBEFORE);
        doc.activeLayer = layer;
        titem = layer.textItem;
        if (titem.desiredGlyphScaling != 100) {
            titem.desiredGlyphScaling = 100;
        }
    }

    layer.name = decodeURI(file.name);
    // app.preferences.typeUnits = TypeUnits.PIXELS;
    titem = layer.textItem;
    titem.contents = caption;

    function _recomputeWidth(textItem) {
        // switching the kind forces PS to recompute the text size
        var contents = textItem.contents;
        textItem.kind = TextType.POINTTEXT;
        textItem.contents = contents;
        textItem.kind = TextType.PARAGRAPHTEXT;

        return Math.round(textItem.parent.bounds[2] - textItem.parent.bounds[0]);
        // return Math.round(textItem.width); Needs to be converted...
    }

    var captionWidth = bnds[2] - bnds[0];

    // Check to see if the caption is too wide
    var twidth = _recomputeWidth(titem);

    if (csOpts.autoscaleCaptions) {
        // try to scale the glyph width to make it fit
        if (twidth > captionWidth) {
            if ((captionWidth / twidth) > 0.75) {
                titem.desiredGlyphScaling = (100 * captionWidth) / twidth;
            } else {
                titem.desiredGlyphScaling = 75;
            }

            twidth = _recomputeWidth(titem);
        }
    }

    if (csOpts.dotTruncate) {
        // truncate the text until it fits
        if (twidth > captionWidth) {
            if (caption.length <= 4) {
                // handle the deviant case here
                titem.contents = caption[0] + '...';

            } else {
                // we need to retrieve the caption from the contents
                // because the initial _recomputeWidth may have truncated
                // the caption. It may be a bug or just odd behaviour
                caption = titem.contents;
                var len = Math.round((captionWidth / twidth) * caption.length);
                var str = caption.substring(0, len - 4) + '...';

                // take off the file extension first
                while (twidth > captionWidth + 4) {
                    titem.contents = str;
                    if (str.length <= 4) {
                        break;
                    }

                    twidth = _recomputeWidth(titem);

                    str = str.substring(0, str.length - 4) + '...';
                }
            }
        }
    }

    var rightV = UnitValue(bnds[2], "px");
    rightV.baseUnit = UnitValue(1 / csOpts.resolution, "in");
    var leftV = UnitValue(bnds[0], "px");
    leftV.baseUnit = UnitValue(1 / csOpts.resolution, "in");

    titem.width = (rightV.as('pt') - leftV.as('pt'));

    var topV = UnitValue(bnds[1], "px");
    topV.baseUnit = UnitValue(1 / csOpts.resolution, "in");

    titem.position = [leftV, topV];

    return layer;
};


//
// Function: insertImageIntoSelection
// Description: Inserts an image into a sheet using the current selection
//              and creates a mask for the image
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
//         layer - the layer into which the image is to be inserted
//         im - the open image or file to be inserted
//         fit - should the image fit the selection bounds or fill the bounds
// Return: the new image layer
//
ContactSheetII.prototype.insertImageIntoSelection = function (doc, csOpts,
    layer, im, fit) {
    var self = this;

    var imageDoc;
    var imageFile;

    if (im instanceof Document) {
        imageDoc = im;

    } else {
        imageFile = psx.convertFptr(im);
    }

    if (fit == undefined) fit = true;

    app.activeDocument = doc;

    if (!psx.hasSelection(doc)) {
        Error.runtimeError(8152); // NoSelection
    }

    var bnds = psx.getSelectionBounds(doc);

    // resize the image doc based on the selection bounds
    var width = bnds[2] - bnds[0];
    var height = bnds[3] - bnds[1];

    if (!imageDoc) {
        if (!imageFile.exists) {
            alert(ZStrings.ImageDoesNotExistFmt.sprintf(imageFile.toUIString()));
            return;
        }
        imageDoc = app.open(imageFile);
    }

    app.activeDocument = imageDoc;
    var lname = imageDoc.name;

    if (imageDoc.mode == DocumentMode.BITMAP) {
        imageDoc.changeMode(ChangeMode.GRAYSCALE);
        psx.copyLayerToDocument(imageDoc, imageDoc.activeLayer, doc);
        psx.undo();

    } else if (imageDoc.mode == DocumentMode.INDEXEDCOLOR ||
        imageDoc.mode == DocumentMode.MULTICHANNEL) {
        imageDoc.changeMode(ChangeMode.RGB);
        psx.copyLayerToDocument(imageDoc, imageDoc.activeLayer, doc);
        psx.undo();

    } else
        psx.copyLayerToDocument(imageDoc, imageDoc.activeLayer, doc); {
    }

    // the rest of this code is the same insertImageIntoBounds

    app.activeDocument = doc;
    var layer = doc.activeLayer;
    layer.name = lname;

    var lbnds = psx.getLayerBounds(doc, layer);
    var lw = lbnds[2] - lbnds[0];
    var lh = lbnds[3] - lbnds[1];

    if (csOpts.bestFit) {
        var deg = (csOpts.rotate == 'CW' ? 90 : -90);

        if (csOpts.landscape && lh > lw) {
            layer.rotate(deg);
        }
        if (!csOpts.landscape && lw > lh) {
            layer.rotate(deg);
        }

        lbnds = psx.getLayerBounds(doc, layer);
        lw = lbnds[2] - lbnds[0];
        lh = lbnds[3] - lbnds[1];
    }

    var orient;

    var lrat = lh / lw;
    var rat = height / width;
    if (fit) {
        if (lrat > rat) {
            orient = 'vert';
        } else {
            orient = 'horz';
        }
    } else { // fill
        if (lrat > rat) {
            orient = 'horz';
        } else {
            orient = 'vert';
        }
    }

    doc.selection.deselect();
    ContactSheetII.transformLayer(doc, layer, bnds, orient);
    psx.selectBounds(doc, bnds);
    psx.createLayerMask(doc, layer, true);

    return layer;
};


// HiPerf Code

// function replaceLayer(file){
//   var desc = new ActionDescriptor();
//   desc.putPath(cTID("null"), new File(file));
//   executeAction(sTID("placedLayerReplaceContents"), desc, DialogModes.NO);
// };


//
// Function: placeImage
// Description: place an image into the active document as a smart object
// Input:  file - the image file to be placed
// Return: the result descriptor
//
function placeImage(file) {
    var desc7 = new ActionDescriptor();
    desc7.putPath(cTID('null'), file);
    desc7.putEnumerated(cTID('FTcs'), cTID('QCSt'), cTID('Qcsa'));
    var desc8 = new ActionDescriptor();
    desc8.putUnitDouble(cTID('Hrzn'), cTID('#Pxl'), 0.000000);
    desc8.putUnitDouble(cTID('Vrtc'), cTID('#Pxl'), 0.000000);
    desc7.putObject(cTID('Ofst'), cTID('Ofst'), desc8);
    return executeAction(cTID('Plc '), desc7, DialogModes.NO);
};


//
// Function: alignThumbnail
// Description: aligns the thumbnail within the cell. For this release,
//              this is always CENTER, BOTTOM
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
//         bnds - the bounds of the current cell
//         layer - the thumbnail layuer
//         fit - should the image fit the selection bounds or fill the bounds
// Return: <none>
//
ContactSheetII.prototype.alignThumbnail = function (doc, csOpts, bnds, layer) {
    var vert = 0;
    var horz = 0;

    if (csOpts.valign == CSII.TOP) {
        vert = -(layer.bounds[1].value - bnds[1]);

    } else if (csOpts.valign == CSII.BOTTOM) {
        vert = layer.bounds[1].value - bnds[1];
    }

    var imageCenter = (bnds[0] + bnds[2]) / 2; // the horizontal center of the image
    var cellCenter = (csOpts.cellBnds[0] + csOpts.cellBnds[2]) / 2;

    // assert(imageCenter == cellCenter);

    if (csOpts.halign == CSII.LEFT) {
        horz = bnds[0] + csOpts.cellBnds[0] - layer.bounds[0].value;

    } else if (csOpts.halign == CSII.RIGHT) {
        horz = -(bnds[0] + csOpts.cellBnds[0] - layer.bounds[0].value);
    }

    if (vert || horz) {
        layer.translate(UnitValue(horz, "px"), UnitValue(vert, "px"));
    }
};

//
// Function: insertImageHP
// Description: Inserts a file into a sheet using the specified bounds.
//              This will use either Place or ViewDocument method
// Input:  doc - the contact sheet
//         csOpts - ContactSheetIIOptions
//         bnds - the rectangle into which the image is to be inserted
//         file - the file to be inserted
// Return: the new image layer or undefined if there was a failure
//
ContactSheetII.prototype.insertImageHP = function (doc, csOpts, bnds, file) {
    var self = this;

    self.imageCount++;

    //   LogFile.write("Inserting image " + decodeURI(file.name) + " at " + bnds);

    app.activeDocument = doc;

    // Add the thumbnail layer and give it the files name
    var layer = doc.artLayers.add();
    var lname = file.strf("%f");

    layer.name = lname;

    var fit = !csOpts.autoFill;

    layer = self.insertImageIntoBounds(doc, layer, csOpts, bnds, file, fit);

    if (!layer) {
        return undefined;
    }

    app.activeDocument = doc;

    self.alignThumbnail(doc, csOpts, bnds, layer);

    return layer;
};

//
// if PLACE_IMAGE is true, images are placed directly from the file to the layer
// if PLACE_IMAGE is false, a ViewDocument is created then copied to the sheet
//
var PLACE_IMAGE = true;

if (!PLACE_IMAGE) {
    var s = psx.SCRIPTS_FOLDER + "/Stack Scripts Only/Terminology.jsx";
    $.evalFile(s);
    var s = psx.SCRIPTS_FOLDER + "/Stack Scripts Only/StackSupport.jsx";
    $.evalFile(s);
}


//
// Function: insertImageIntoBounds
// Description: Inserts a file into a sheet using the specified bounds.
//              This will use either Place or ViewDocument method
// Input:  doc - the contact sheet
//         layer - the layer where the image file will be inserted
//         csOpts - ContactSheetIIOptions
//         bnds - the rectangle into which the image is to be inserted
//         file - the file to be inserted
//         fit - should the image fit the selection bounds or fill the bounds
// Return: the new image layer or undefined if there was a failure
//
ContactSheetII.prototype.insertImageIntoBounds = function (doc, layer, csOpts,
    bnds, file, fit) {
    var self = this;

    if (fit == undefined) fit = true;

    var width = bnds[2] - bnds[0];
    var height = bnds[3] - bnds[1];

    if (!file.exists) {
        var msg = ZStrings.ImageDoesNotExistFmt.sprintf(file.toUIString());
        LogFile.write(msg);
        return undefined;
    }

    var f = file;

    if (PLACE_IMAGE) {
        try {
            var desc = placeImage(f);

            layer = doc.activeLayer;

            if (f != file) {
                f.remove();
            }

            try {
                layer.resize(100, 100, AnchorPosition.MIDDLECENTER);

            } catch (e) {
                if (e.number == 8007) {      // User cancelled
                    try {
                        layer.resize(100, 100, AnchorPosition.MIDDLECENTER);
                    } catch (e) {
                        if (e.number != 8007) {      // User cancelled
                            Error.runtimeError(9001, e.toString());
                        }
                    }
                } else {
                    Error.runtimeError(9001, e.toString());
                }
            }

        } catch (e) {
            if (e.number == 8007) { // User cancelled the operation
                Error.runtimeError(8007);
            }

            var msg = ('Image file not placed: ' + file + ' - ' + e.message +
                '@' + e.line);
            LogFile.write(msg);
            LogFile.logException(e);
            self.errorCount++;

            // try some error recovery...
            Error.runtimeError(CSII.kFileOpenError, ZStrings.ErrorInProcessing);
            return undefined;
        }

        function _rasterize() {
            var desc = new ActionDescriptor();
            var ref = new ActionReference();
            ref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
            desc.putReference(cTID('null'), ref);
            executeAction(sTID('rasterizeLayer'), desc, DialogModes.NO);
        };

        if (!csOpts.embedSmartObjects) {
            _rasterize(doc, layer);
        }

    } else {
        // Use ViewlessDocument code from StackSupport
        // This currently does not work
        var vd = openViewlessDocument(f.fsName);
        vd.addToActiveDocument();
        if (layer != doc.activeLayer) {
            psx.mergeDown(doc);
        }
        layer = doc.activeLayer;
    }

    var lname = layer.name;

    var lbnds = psx.getLayerBounds(doc, layer);
    var lw = lbnds[2] - lbnds[0];
    var lh = lbnds[3] - lbnds[1];

    if (csOpts.bestFit) {
        var deg = (csOpts.rotate == CSII.CW ? 90 : -90);

        if (csOpts.landscape && lh > lw) {
            layer.rotate(deg);
        }
        if (!csOpts.landscape && lw > lh) {
            layer.rotate(deg);
        }

        lbnds = psx.getLayerBounds(doc, layer);
        lw = lbnds[2] - lbnds[0];
        lh = lbnds[3] - lbnds[1];
    }

    var orient;

    var lrat = lh / lw;
    var rat = height / width;
    if (fit) {
        if (lrat > rat) {
            orient = 'vert';
        } else {
            orient = 'horz';
        }
    } else { // fill
        if (lrat > rat) {
            orient = 'horz';
        } else {
            orient = 'vert';
        }
    }

    ContactSheetII.transformLayer(doc, layer, bnds, orient);
    psx.selectBounds(doc, bnds);
    psx.createLayerMask(doc, layer, true);
    doc.selection.deselect();

    return layer;
};


//
// Function: transformLayer
// Description: transform a layer to fit the bounds indicated. If there
//              was a problem populating the layer, the space defined
//              by bnds will be filled grey.
// Input:  doc - the contact sheet
//         layer - the layer where the image file will be inserted
//         bnds - the rectangle into which the image is to be inserted
//         orient - 'vert' or 'horz'. Needed for fit/fill setting.
// Return: the new image layer or undefined if there was a failure
//
ContactSheetII.transformLayer = function (doc, layer, bnds, orient) {
    var lbnds = psx.getLayerBounds(doc, layer);

    var newW = bnds[2] - bnds[0];
    var newH = bnds[3] - bnds[1];
    var oldW = lbnds[2] - lbnds[0];
    var oldH = lbnds[3] - lbnds[1];

    var hrzn = bnds[0] - (lbnds[0] - (newW - oldW) / 2);
    var vrtc = bnds[1] - (lbnds[1] - (newH - oldH) / 2);

    if (oldW == 0 || oldH == 0) {
        psx.selectBounds(doc, bnds);
        doc.selection.fill(psx.COLOR_GRAY);
        return;
    }

    var prc = 0;
    if (orient == 'horz') {
        prc = (newW / oldW) * 100;
    } else {
        prc = (newH / oldH) * 100;
    }

    function _ftn() {
        var desc = new ActionDescriptor();
        var ref = new ActionReference();
        ref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
        desc.putReference(cTID('null'), ref);
        desc.putEnumerated(cTID('FTcs'), cTID('QCSt'), cTID('Qcsa'));
        var ldesc = new ActionDescriptor();
        ldesc.putUnitDouble(cTID('Hrzn'), cTID('#Pxl'), hrzn);
        ldesc.putUnitDouble(cTID('Vrtc'), cTID('#Pxl'), vrtc);
        desc.putObject(cTID('Ofst'), cTID('Ofst'), ldesc);
        desc.putUnitDouble(cTID('Wdth'), cTID('#Prc'), prc);
        desc.putUnitDouble(cTID('Hght'), cTID('#Prc'), prc);
        executeAction(cTID('Trnf'), desc, DialogModes.NO);
    };

    app.activeDocument = doc;
    doc.activeLayer = layer;
    _ftn();
};

"CSIILayout.jsx";
// EOF

//
// CSIIExt
//
// $Id: CSIIExt.jsx,v 1.6 2012/03/15 17:26:57 anonymous Exp $
//
//


//
// UI constants
//
ContactSheetUI.CSII_WIDTH = 600;
ContactSheetUI.CSII_HEIGHT = 665;

ContactSheetUI.MIN_COLS_ROWS = 1;
ContactSheetUI.MAX_COLS_ROWS = 100;
ContactSheetUI.CM_PER_INCH = 2.54;
ContactSheetUI.MM_PER_INCH = 25.4;
ContactSheetUI.MIN_RES_INCHES = 35.0;
ContactSheetUI.MAX_RES_INCHES = 1200.0;
ContactSheetUI.MIN_RES_CM =
    (ContactSheetUI.MIN_RES_INCHES / ContactSheetUI.CM_PER_INCH);
ContactSheetUI.MAX_RES_CM =
    (ContactSheetUI.MAX_RES_INCHES / ContactSheetUI.CM_PER_INCH);
ContactSheetUI.MIN_RES_MM =
    (ContactSheetUI.MIN_RES_INCHES / ContactSheetUI.MM_PER_INCH);
ContactSheetUI.MAX_RES_MM =
    (ContactSheetUI.MAX_RES_INCHES / ContactSheetUI.MM_PER_INCH);
ContactSheetUI.MIN_PIXELS = 100;
ContactSheetUI.MAX_PIXELS = 29000;

ContactSheetUI.DEFAULT_FONT_SIZE = 12;
ContactSheetUI.MIN_FONTSIZE = 4;
ContactSheetUI.MAX_FONTSIZE = 72;

ContactSheetUI.PS_MIN_DESIRED_X_GAP = 20;
ContactSheetUI.PS_MIN_DESIRED_Y_GAP = 20;

ContactSheetUI.notes = undefined;

//
// Function: createPanel
// Description: Creates the subpanels in the CSII Dialog
// Input:  pnl - Panel
//         ini - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createPanel = function (pnl, ini) {
    var self = this;
    var opts = new ContactSheetIIOptions(ini);

    pnl.window.processor = self;
    pnl.mgr = self;

    // var bnds = pnl.window.bounds;
    // self.winRect.w = bnds.width = 560;
    // self.winRect.h = bnds.height = 600;

    self.textOfs = 2;
    self.menuWidth = 120;
    self.textWidth = 80;
    self.lineH = 28;
    self.xOfs = 5;
    self.gutter = 90;

    var panelWidth = 435;

    var yOfs = 0;
    var xOfs = self.xOfs;
    var xx = xOfs;
    var yy = yOfs;

    pnl.sourcePanel = pnl.add('panel', [xx, yy, xx + panelWidth, yy + 260],
        ZStrings.SourceImages, { name: 'SourcePanel' });
    self.createCSIISourcePanel(pnl.sourcePanel, opts);

    yy += pnl.sourcePanel.bounds.height + 10;

    pnl.docPanel = pnl.add('panel', [xx, yy, xx + panelWidth + 135, yy + 165],
        ZStrings.Document,
        { name: 'docPanel' });
    self.createCSIIDocumentPanel(pnl.docPanel, opts);

    yy += pnl.docPanel.bounds.height + 10;

    pnl.thumbnailPanel = pnl.add('panel', [xx, yy, xx + panelWidth + 135, yy + 135],
        ZStrings.Thumbnails,
        { name: 'thumbnailPanel' });
    self.createCSIIThumbnailPanel(pnl.thumbnailPanel, opts);

    yy += pnl.thumbnailPanel.bounds.height + 10;

    pnl.captionPanel = pnl.add('panel', [xx, yy, xx + panelWidth + 135, yy + 50],
        ZStrings.FilenameAsCaption,
        { name: 'captionPanel' });
    self.createCSIICaptionPanel(pnl.captionPanel, opts);

    var maxW = Math.max(pnl.thumbnailPanel.bounds.width,
        pnl.captionPanel.bounds.width);
    pnl.thumbnailPanel.bounds.width = pnl.captionPanel.bounds.width = maxW;

    pnl.ctrlPanel = pnl.add('group',
        [xx + panelWidth + 5, 0, pnl.bounds.width - 5, 400],
        { name: 'ctrlPanel' });
    self.createCSIIControlPanel(pnl.ctrlPanel);

    return pnl;
};

//
// Function: _fixMaxWidth
// Description: Returns the widest display width from an array of strings
// Input:  pnl - Panel
//         ary - Array of UI strings
// Return: int
//
function _findMaxWidth(pnl, ary) {
    var maxW = 0;
    for (var i = 0; i < ary.length; i++) {
        var w = pnl.graphics.measureString(ary[i])[0];
        maxW = Math.max(maxW, w);
    }
    return maxW;
};

//
// Function: createCSIISourcePanel
// Description: Creates the Source pnl
// Input:  pnl - Panel
//         opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createCSIISourcePanel = function (pnl, opts) {
    var self = this;

    LogFile.write("Creating Source Panel");

    self.sourcePanel = pnl;

    var xOfs = 10;
    var yOfs = 12;
    var gutter1 = 40;
    var gutter = 90

    var x = xOfs;
    var y = yOfs;

    var w = pnl.bounds.width;

    var width = 125;

    pnl.mgr = this;

    var maxLen = _findMaxWidth(pnl, [ZStrings.Choose, ZStrings.UseLabel]);

    if (maxLen + 15 > gutter) {
        gutter = maxLen + 15;
    }

    // Image Source
    pnl.useLbl = pnl.add('statictext',
        [x, y + self.textOfs, x + gutter - 10, y + 22 + self.textOfs],
        ZStrings.UseLabel,
        { name: 'imageSourceLabel' });
    pnl.useLbl.helpTip = ZStrings.UseTip;
    x += gutter;

    var srcsel = [ZStrings.Files, ZStrings.Folder];

    if (app.documents.length > 0) {
        srcsel.push(ZStrings.CurrentDocs);
    }

    if (ContactSheetII.bridgeIsRunning) {
        srcsel.push(ZStrings.Bridge);
    }

    if (ContactSheetII.runningFromBridge == true) {
        srcsel = [ZStrings.Bridge];
    }

    var maxW = _findMaxWidth(pnl, srcsel);
    var menuWidth = Math.max(maxW + 35, width);

    pnl.imageSource = pnl.add('dropdownlist', [x, y, x + menuWidth, y + 22], srcsel,
        { name: 'imageSource' });

    pnl.imageSource.selection = 0;
    pnl.imageSource.helpTip = ZStrings.UseTip;
    y += 28;
    x = xOfs;

    // adjust the button width if we have long strings in a particular locale
    var btnH = 25;
    var btnW = 130;
    // var btnW = psx.delocalize(ZStrings.SourcePnlBtnWidth, 120);
    // btnW = 140; // locale testing

    var btns = [ZStrings.Browse, ZStrings.Remove,
    ZStrings.AddOpenFiles, ZStrings.AddBridgeFiles, ZStrings.SortByFilename];

    var maxW = _findMaxWidth(pnl, btns);

    btnW = Math.max(maxW + 10, btnW);


    var fileListWidth = pnl.bounds.width - x - 20 - btnW;

    pnl.fileList = pnl.add('listbox', [x, y, x + fileListWidth, y + 197], undefined,
        { name: 'fileList', multiselect: true });

    pnl.fileList.onDoubleClick = function () {
        var pnl = this.parent;
        ContactSheetII.caseInsensitiveFileSort(pnl.fileSet);
        _updateFileList(pnl, pnl.fileSet);
    }

    x += fileListWidth + 10;

    pnl.browseBtn = pnl.add('button', [x, y, x + btnW, y + btnH], ZStrings.Browse);
    y += btnH + 5;
    pnl.removeBtn = pnl.add('button', [x, y, x + btnW, y + btnH], ZStrings.Remove);
    y += btnH + 5;
    pnl.addOpenBtn = pnl.add('button', [x, y, x + btnW, y + btnH],
        ZStrings.AddOpenFiles);
    y += btnH + 5;
    pnl.addBridgeBtn = pnl.add('button', [x, y, x + btnW, y + btnH],
        ZStrings.AddBridgeFiles);

    y += btnH + 5;
    pnl.sortBtn = pnl.add('button', [x, y, x + btnW, y + btnH],
        ZStrings.SortByFilename);

    pnl.fileWidgetList = [pnl.fileList, pnl.browseBtn, pnl.removeBtn,
    pnl.addOpenBtn, pnl.addBridgeBtn, pnl.sortBtn];

    y = yOfs + 28;
    x = xOfs;

    pnl.selNote = pnl.add('statictext',
        [x + gutter1, y + self.textOfs, w - xOfs, y + 22 + self.textOfs],
        '', { name: 'selectionNote' });
    // Images
    pnl.browse = pnl.add('button', [x, y, x + gutter - 10, y + 22], ZStrings.Choose);
    pnl.browse.helpTip = ZStrings.ChooseSourceFolderTip;
    x += gutter;

    var textType = (isMac() ? 'statictext' : 'edittext');
    var xtofs = (isMac() ? 3 : 0);

    pnl.folder = pnl.add(textType, [x, y + xtofs, w - 10, y + 23 + xtofs], '',
        { readonly: true, name: 'sourceFolder' });
    pnl.folder.helpTip = ZStrings.ChooseSourceFolderTip;

    y += 30;
    x = xOfs;

    // Subfolders
    pnl.recurse = pnl.add('checkbox', [x, y, pnl.bounds.width - x, y + 25],
        ZStrings.IncludeSubfolders,
        CSIIDefaults.includeSubfolders,
        { name: 'recurse' });
    pnl.recurse.helpTip = ZStrings.IncludeSubfoldersTip;

    y += 30;

    // Group
    pnl.group = pnl.add('checkbox', [x, y, pnl.bounds.width - x, y + 25],
        ZStrings.GroupByFolder,
        CSIIDefaults.groupImages,
        { name: 'groupByFolder' });
    pnl.group.helpTip = ZStrings.GroupByFolderTip;

    pnl.imageSource.onChange = function () {
        var pnl = this.parent;
        var sel = pnl.imageSource.selection.text;

        var isFolder = (sel == ZStrings.Folder);

        pnl.browse.enabled = isFolder;
        pnl.folder.enabled = isFolder;
        pnl.recurse.enabled = isFolder;
        pnl.group.enabled = isFolder && pnl.recurse.value;

        if (sel == ZStrings.Bridge) {
            var files = ContactSheetUI.getImageFilesFromBridge();
            if (files.length == 1) {
                var txt = ZStrings.FileSelectedFmt.sprintf(files.length);
            } else {
                var txt = ZStrings.FilesSelectedFmt.sprintf(files.length);
            }
            pnl.selNote.text = txt;
        }
        if (sel == ZStrings.CurrentDocs) {
            if (app.documents.length == 1) {
                var txt = ZStrings.FileSelectedFmt.sprintf(app.documents.length);
            } else {
                var txt = ZStrings.FilesSelectedFmt.sprintf(app.documents.length);
            }
            pnl.selNote.text = txt;
        }

        pnl._setUIState();
    }

    pnl.fileSet = [];

    function _updateFileList(pnl, files) {
        pnl.fileList.removeAll();

        for (var i = 0; i < files.length; i++) {
            pnl.fileList.add('item', File.decode(files[i].name));
        }
    }
    function _addFileToList(pnl, files, f) {
        if (f == null) {
            return;
        }
        var fstr;
        if (f.typename == "Document") {
            fstr = name;

        } else {
            if (typeof (f) != File) {
                f = File(f.toString());
            }
            if (!psx.isValidImageFile(f)) {
                return;
            }
            fstr = f.absoluteURI;
        }

        var i = 0;
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            if (f.typename == "Document") {
                if (f == file) {
                    return;
                }
            } else {
                var estr = file.absoluteURI;
                if (fstr == estr) {
                    return;
                }
            }
        }

        files.push(f);
        //     ContactSheetII.caseInsensitiveFileSort(files);
    }

    pnl.removeBtn.onClick = function () {
        var pnl = this.parent;
        var list = pnl.fileList.selection;
        var fileSet = pnl.fileSet;
        if (!list || list.length == 0) {
            return;
        }
        for (var i = 0; i < list.length; i++) {
            var el = list[i];
            var txt = el.text;
            for (var j = 0; j < fileSet.length; j++) {
                var file = fileSet[j];
                var fname = File.decode(file.name);
                if (fname == txt) {
                    fileSet.splice(j, 1);
                    break;
                }
            }
            pnl.fileList.remove(el);
        }

        pnl._setUIState();
    }

    pnl.browseBtn.onClick = function () {
        var pnl = this.parent;
        var sel = pnl.imageSource.selection.text;

        try {
            if (sel == ZStrings.Files) {
                var files = photoshopFileOpenDialog();

                if (files.length) {
                    ContactSheetII.caseInsensitiveSort(files);

                    for (var i = 0; i < files.length; i++) {
                        var f = files[i];
                        _addFileToList(pnl, pnl.fileSet, f);
                    }
                }

                _updateFileList(pnl, pnl.fileSet);

            } else if (sel == ZStrings.Folder) {
                var str = ZStrings.FolderSelect;
                var folder = Folder.selectDialog(str);
                if (folder) {
                    var sel = isMac() ? psx.macFileSelection : psx.winFileSelection;
                    var files = folder.getFiles(sel);
                    ContactSheetII.caseInsensitiveFileSort(files);

                    for (var i = 0; i < files.length; i++) {
                        var f = files[i];
                        _addFileToList(pnl, pnl.fileSet, f);
                    }

                    _updateFileList(pnl, pnl.fileSet);
                }
            }
        } catch (e) {
            alert(psx.exceptionMessage(e));
        }

        pnl._setUIState();
    }

    pnl.addOpenBtn.onClick = function () {
        var pnl = this.parent;
        var docs = app.documents;

        for (var i = 0; i < docs.length; i++) {
            var doc = docs[i];

            _addFileToList(pnl, pnl.fileSet, doc);
        }

        _updateFileList(pnl, pnl.fileSet);
    }

    pnl.sortBtn.onClick = function () {
        var pnl = this.parent;
        ContactSheetII.caseInsensitiveFileSort(pnl.fileSet);
        _updateFileList(pnl, pnl.fileSet);
    }

    pnl.addBridgeBtn.onClick = function () {
        var pnl = this.parent;
        var files = ContactSheetUI.getImageFilesFromBridge();
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            _addFileToList(pnl, pnl.fileSet, file);
        }

        _updateFileList(pnl, pnl.fileSet);
    }

    if (opts.folder) {
        pnl.folder._folder = new Folder(opts.folder).toUIString();
        pnl.folder.text = ContactSheetUI.shortFileName(pnl.folder._folder);

    } else {
        pnl.folder._folder = undefined;
        pnl.folder.text = '';
    }
    pnl.recurse.value = toBoolean(opts.recurse);
    pnl.group.value = !toBoolean(opts.spanFolders);

    pnl.browse.onClick = function () {
        try {
            var pnl = this.parent;
            var def;
            if (pnl.folder._folder) {
                def = pnl.folder._folder;
            } else {
                def = Folder.desktop;
            }
            var f = psx.selectFolder(ZStrings.BrowseForFolder, def);
            if (f) {
                var fname = decodeURI(f.fsName);
                pnl.folder._folder = f;
                pnl.folder.text = ContactSheetUI.shortFileName(fname);
            }
        } catch (e) {
            alert(psx.exceptionMessage(e));
        }
    };

    pnl.recurse.onClick = function () {
        var pnl = this.parent;
        pnl.group.enabled = pnl.recurse.value;
    };

    pnl._setUIState = function () {
        try {
            var pnl = this;
            var sel = pnl.imageSource.selection.text;
            var isFiles = (sel == ZStrings.Files);
            var isFolder = (sel == ZStrings.Folder);
            var len = pnl.children.length;

            for (var i = 0; i < len; i++) {
                var child = pnl.children[i];
                if (child.properties && child.properties.name &&
                    child.properties.name.contains('imageSource')) {
                    continue;
                }

                if (isFiles) {
                    child.visible = pnl.fileWidgetList.contains(child);
                } else if (isFolder) {
                    child.visible = !pnl.fileWidgetList.contains(child);
                } else {
                    child.visible = false;
                }
            }

            pnl.selNote.visible = !(isFiles || isFolder);

            // pnl.removeBtn.enabled = (pnl.fileSet.length > 0
            //                          && pnl.fileList.selection);
            pnl.addOpenBtn.enabled = app.documents.length > 0;
            pnl.addBridgeBtn.enabled = BridgeTalk.isRunning("bridge");

        } catch (e) {
            alert(psx.exceptionMessage(e));
        }
    }

    pnl.imageSource.onChange();

    return pnl;
};


//
// Function: _approximate
// Description: Approximates a number for the UI
// Input:  f - number
// Return: number or UnitValue
//
function _approximate(f) {
    var p = 3;

    if (f instanceof UnitValue) {
        var un = UnitValue(f.toString());
        f = un.value;
        var v = Number(f.toFixed(3));
        un = UnitValue(v, un.type);

        if (un.type == "px") {
            if (un.value == 0) {
                un.value = 1;
            } else {
                un.value = Math.round(un.value);
            }
        }

        return un;

    } else {
        return Number(f.toFixed(3));
    }
};

//
// Function: updateUnitWidget
// Description: Set the value of a numeric UI field
// Input:  obj - UI widget
//         type - UnitValue type
//         base - the resolution base (opt)
// Return: number or UnitValue
//
ContactSheetUI.prototype.updateUnitWidget = function (obj, type, base) {
    var pnl = obj.parent;

    var un = UnitValue(obj._last, "px");

    if (!base) {
        base = UnitValue(1 / 72, "in");
    }

    un.baseUnit = base;
    var val = un.as(type);

    if (type == "px") {
        // var processor = pnl.window.processor;
        // if (pnl == processor.docPnl) {
        //   var rez = psx.delocalizeNumber(pnl.resolution.text);
        //   var lastUnit = UnitValue(psx.delocalizeNumber(obj.text), pnl.units._last);
        //   lastUnit.baseUnit = base;

        //   if (lastUnit.type == "in") {
        //     val = rez * lastUnit.value;
        //   } else {
        //     val = rez * lastUnit.value * ContactSheetUI.CM_PER_INCH;
        //   }

        //   un.value = val;
        //   obj._last = un.as("px");

        // } else {
        val = Math.round(val);
    }

    if (obj.type == "editnumber")
        obj.value = _approximate(val);
    else
        obj.text = psx.localizeNumber(_approximate(val));

    return;
};

//
// Function: currentUnits
// Description:  current Units in delocalized form
// Input:  <none>
// Return: delocalized Units value
//
ContactSheetUI.prototype.currentUnits = function () {
    var self = this;
    return delocalizeUnitType(self.docPnl.units.selection.text);
};

//
// Function: currentLocalizedUnits
// Description: current Units in localized form in short form
// Input:  <none>
// Return: localized Units value
//
ContactSheetUI.prototype.currentLocalizedUnits = function () {
    var self = this;
    var type = delocalizeUnitType(self.docPnl.units.selection.text);
    var un = UnitValue(0, type);
    return psx.localizeUnitValue(un).type;
};


//
// Function: handleEnterKey
// Description: This function eats Enter keys in edittext widgets
//              The onChange callback is temporarily disabled so
//              that it does not get called if the function throws
//              up an alert which would cause another onChange
//              event to get fired because of the loss of focus
//  Input: event - a UI event
//  Return: <none>
//
ContactSheetUI.handleEnterKey = function (event) {
    var obj = event.currentTarget;

    if (event.keyName == 'Enter') {
        if (obj.onChange) {
            obj._ftn = obj.onChange;
            obj.onChange = undefined;
            var res = obj._ftn();
            obj.onChange = obj._ftn;
            obj._ftn = undefined;

            // if the field validation failed, eat the Enter key
            if (res == false) {
                event.stopPropagation();
                event.preventDefault();

            } else if (res == true) {
                // if it was valid, run the script

            } else {
                // we didn't validate
            }
        }
    }
};

//
// Function: createCSIIDocumentPanel
// Description: Creates the Document panel
// Input:  pnl - Panel
//         opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createCSIIDocumentPanel = function (pnl, opts) {
    var self = this;

    var xOfs = 10;
    var yOfs = 12;
    var xx = xOfs;
    var yy = yOfs;
    var tOfs = self.textOfs;
    var menuW = self.menuWidth;
    var textW = self.textWidth;
    var lineH = self.lineH;
    var gutter = 90;
    // var gutter = toNumber(ZStrings.DocumentPnlCol2, 90);
    // gutter = 140; // locale testing

    self.docPnl = pnl;

    // adjust the gutter width if there is a long localized label strings
    var labelsLeft = [ZStrings.UnitsLabel, ZStrings.WidthLabel, ZStrings.HeightLabel,
    ZStrings.ResolutionLabel, ZStrings.ColorProfileLabel];

    var labelsRight = [ZStrings.ModeLabel, ZStrings.BitDepthLabel];

    var maxWLeft = _findMaxWidth(pnl, labelsLeft);
    var maxWRight = _findMaxWidth(pnl, labelsRight);

    var x = gutter;
    var gutterL = Math.max(maxWLeft + 5, gutter);
    var gutterR = Math.max(maxWRight + 5, gutter);

    function _resAsInches(val, type) {
        val = psx.delocalizeNumber(val);
        return ((type == ZStrings.PixelsPerInch) ?
            val : (val / ContactSheetUI.CM_PER_INCH));
    }

    pnl._baseUnit = new UnitValue(1 / 300, "in");

    pnl.toPixels = function (val, type) {
        var pnl = this;
        if (isString(val)) {
            val = psx.delocalizeNumber(val);
        }
        val = toNumber(val) || undefined;
        var un = new UnitValue(val, type);
        un.baseUnit = pnl._baseUnit;
        return Math.round(un.as('px'));
    }

    pnl.getResolution = function () {
        var pnl = this;
        var val = pnl.resolution.value;
        var type = ((pnl.resUnits.selection.text == ZStrings.PixelsPerInch) ?
            "in" : "cm");
        return UnitValue(val, type);
    }

    pnl.getWidth = function () {
        return pnl.width.value;
    }
    pnl.getHeight = function () {
        return pnl.height.value;
    }

    //
    // Units
    //
    pnl.unitsLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterL, yy + 22 + tOfs],
        ZStrings.UnitsLabel);
    pnl.unitsLabel.helpTip = ZStrings.UnitsTip;

    xx += gutterL;
    pnl.units = pnl.add('dropdownlist', [xx, yy, xx + menuW, yy + 22],
        [ZStrings.Inches,
        ZStrings.Centimeter,
        ZStrings.Pixels],
        { name: 'units' });
    pnl.units.helpTip = ZStrings.UnitsTip;

    psxui.setMenuSelection(pnl.units, CSIIDefaults.units, undefined, true);
    pnl.units._last = UnitValue(0, delocalizeUnitType(CSIIDefaults.units)).type;

    pnl.units.onChange = function () {
        try {
            var pnl = this.parent;
            var ltype = pnl.units.selection.text;
            var processor = pnl.window.processor;
            var last = pnl.units._last;

            var type = UnitValue(0, delocalizeUnitType(ltype)).type;

            // change the min and max value of pnl.width
            pnl.width.minvalue = ContactSheetUI.convertValueToUnit(ContactSheetUI.MIN_PIXELS, "px", type, pnl._baseUnit);
            pnl.width.maxvalue = ContactSheetUI.convertValueToUnit(ContactSheetUI.MAX_PIXELS, "px", type, pnl._baseUnit);
            processor.updateUnitWidget(pnl.width, type, pnl._baseUnit);
            // change the min and max value of pnl.height
            pnl.height.minvalue = ContactSheetUI.convertValueToUnit(ContactSheetUI.MIN_PIXELS, "px", type, pnl._baseUnit);
            pnl.height.maxvalue = ContactSheetUI.convertValueToUnit(ContactSheetUI.MAX_PIXELS, "px", type, pnl._baseUnit);
            processor.updateUnitWidget(pnl.height, type, pnl._baseUnit);

            if (processor.thumbnailPnl) {
                var th = processor.thumbnailPnl;

                processor.updateUnitWidget(th.vert, type);
                th.vert.text += ' ' + processor.currentLocalizedUnits();

                // processor.thumbnailPnl.vert.onChange();

                processor.updateUnitWidget(th.horz, type);
                th.horz.text += ' ' + processor.currentLocalizedUnits();

                // processor.thumbnailPnl.horz.onChange();

                pnl.units._last = type;
            }

        } catch (e) {
            alert(psx.exceptionMessage(e));
        }
    }

    yy += lineH;
    xx = xOfs;

    var unitType = delocalizeUnitType(pnl.units.selection.text);

    //
    // Width
    //
    pnl.widthLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterL, yy + 22 + tOfs],
        ZStrings.WidthLabel);
    pnl.widthLabel.helpTip = ZStrings.WidthTip;
    xx += gutterL;
    pnl.width = pnl.add('editnumber', [xx, yy, xx + textW, yy + 20],
        opts.docWidth || CSIIDefaults.width,
        ContactSheetUI.convertValueToUnit(ContactSheetUI.MIN_PIXELS, "px", unitType, pnl._baseUnit),
        ContactSheetUI.convertValueToUnit(ContactSheetUI.MAX_PIXELS, "px", unitType, pnl._baseUnit),
        { name: 'width', decimalDigits: 3 });
    pnl.width._last = pnl.toPixels(pnl.width.value, unitType);
    pnl.width.helpTip = ZStrings.WidthTip;

    pnl.width.onChange = function () {
        var pnl = this.parent;
        var type = delocalizeUnitType(pnl.units.selection.text);
        var n = pnl.width.value;
        pnl.width._last = pnl.toPixels(n, type);
        return true;
    }

    // pnl.width.onChanging = psxui.positiveNumberKeystrokeFilter;
    // pnl.width.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    // we have used editnumber control for pnl.width, so blocking of invalid input will be done internally
    //ContactSheetUI.addPositiveNumberFilter(pnl.width);

    //
    // Height
    //
    yy += lineH;
    xx = xOfs;

    pnl.heightLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterL, yy + 22 + tOfs],
        ZStrings.HeightLabel,
        { justify: 'right' });
    pnl.heightLabel.helpTip = ZStrings.HeightTip;
    // The right justification bit does not work...

    xx += gutterL;
    pnl.height = pnl.add('editnumber', [xx, yy, xx + textW, yy + 20],
        opts.docHeight || CSIIDefaults.height,
        ContactSheetUI.convertValueToUnit(ContactSheetUI.MIN_PIXELS, "px", unitType, pnl._baseUnit),
        ContactSheetUI.convertValueToUnit(ContactSheetUI.MAX_PIXELS, "px", unitType, pnl._baseUnit),
        { name: 'height', decimalDigits: 3 });
    pnl.height.helpTip = ZStrings.HeightTip;
    pnl.height._last = pnl.toPixels(pnl.height.value, unitType);

    pnl.height.onChange = function () {
        var pnl = this.parent;
        var type = delocalizeUnitType(pnl.units.selection.text);

        var n = pnl.height.value;
        pnl.height._last = pnl.toPixels(n, type);
        return true;
    }

    // pnl.height.onChanging = psxui.positiveNumberKeystrokeFilter;
    // pnl.height.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    // we have used editnumber control for pnl.height, so blocking of invalid input will be done internally
    //ContactSheetUI.addPositiveNumberFilter(pnl.height);

    yy += lineH;
    xx = xOfs;

    //
    // Resolution
    //
    pnl.resolutionLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterL, yy + 22 + tOfs],
        ZStrings.ResolutionLabel);
    pnl.resolutionLabel.helpTip = ZStrings.ResolutionTip;
    xx += gutterL;

    pnl.resolution = pnl.add('editnumber', [xx, yy, xx + textW, yy + 20],
        opts.resolution || CSIIDefaults.resolution,
        undefined,
        undefined,
        { name: 'resolution', decimalDigits: 3 });
    pnl.resolution.helpTip = ZStrings.ResolutionTip;

    pnl.resolution.onChange = function (unitsOnly) {
        try {
            var pnl = this.parent;
            var processor = pnl.window.processor;
            var last = pnl.resolution._value;
            var resType = ((pnl.resUnits.selection.text == ZStrings.PixelsPerInch) ?
                ZStrs.Units_IN : ZStrs.Units_CM);

            var res = pnl.resolution.value;
            var base = new UnitValue(1 / res, psx.delocalizeUnitType(resType));
            pnl._baseUnit = base;

            if (!unitsOnly) {
                if (resType == ZStrs.Units_IN) {
                    pnl.resolution._inches = res;
                } else {
                    pnl.resolution._inches = res * ContactSheetUI.CM_PER_INCH;
                }
            }

            pnl.resolution._value = res;
            var units = processor.currentUnits();

            if (units != "px") {
                var w = pnl.getWidth();
                var h = pnl.getHeight();
                var un = UnitValue(0, units);
                un.baseUnit = pnl._baseUnit;

                un.value = w;
                pnl.width._last = un.as("px");
                un.value = h;
                pnl.height._last = un.as("px");
            }

            return true;

        } catch (e) {
            alert(psx.exceptionMessage(e));
            return false;
        }
    }

    // pnl.resolution.onChanging = psxui.positiveNumberKeystrokeFilter;
    // pnl.resolution.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    ContactSheetUI.addPositiveNumberFilter(pnl.height);
    // we have used editnumber control for pnl.resolution, so blocking of invalid input will be done internally
    //ContactSheetUI.addPositiveNumberFilter(pnl.resolution);

    xx += pnl.resolution.bounds.width + 5;

    //
    // Res Units
    //
    pnl.resUnits = pnl.add('dropdownlist', [xx, yy, xx + menuW, yy + 22],
        [ZStrings.PixelsPerInch, ZStrings.PixelsPerCM],
        { name: 'resUnits' });
    pnl.resUnits.helpTip = ZStrings.ResolutionTip;

    psxui.setMenuSelection(pnl.resUnits, CSIIDefaults.resUnits, undefined, true);

    // set min and max value for resolution
    var resType = ((pnl.resUnits.selection.text == ZStrings.PixelsPerInch) ?
        ZStrs.Units_IN : ZStrs.Units_CM);

    if (resType == ZStrs.Units_IN) {
        pnl.resolution.minvalue = ContactSheetUI.MIN_RES_INCHES;
        pnl.resolution.maxvalue = ContactSheetUI.MAX_RES_INCHES;
    } else {
        pnl.resolution.minvalue = ContactSheetUI.MIN_RES_CM;
        pnl.resolution.maxvalue = ContactSheetUI.MAX_RES_CM;
    }

    pnl.resUnits.onChange = function () {
        var pnl = this.parent;
        var resType = ((pnl.resUnits.selection.text == ZStrings.PixelsPerInch) ?
            ZStrs.UnitsIN : ZStrs.UnitsCM);

        var res = pnl.resolution.value;

        if (resType == ZStrs.UnitsCM) {
            pnl.resolution.minvalue = ContactSheetUI.MIN_RES_CM;
            pnl.resolution.maxvalue = ContactSheetUI.MAX_RES_CM;
            pnl.resolution.value = _approximate(res / 2.54);
        } else {
            pnl.resolution.minvalue = ContactSheetUI.MIN_RES_INCHES;
            pnl.resolution.maxvalue = ContactSheetUI.MAX_RES_INCHES;
            pnl.resolution.value = _approximate(pnl.resolution._inches);
        }

        pnl.resolution.onChange(true);
    }

    var type = delocalizeUnitType(pnl.resUnits.selection.text);

    pnl.resolution._value = pnl.resolution.value;
    pnl.resolution._inches = _resAsInches(pnl.resolution.value, type);

    pnl.resolution.onChange(true);

    var yy_saved = yy + lineH;
    //xOfs = gutterL + menuW + 5 + xOfs + 50;
    xOfs = Math.max(300, (gutterL + menuW + 5 + xOfs + 50));

    yy = yOfs;
    xx = xOfs;

    //
    // Flatten All Layers
    //
    pnl.flattenAllLayers = pnl.add('checkbox', [xx, yy, pnl.bounds.width - 10, yy + 22],
        ZStrings.FlattenAllLayers,
        { name: 'flattenAllLayers' });
    pnl.flattenAllLayers.helpTip = ZStrings.FlattenAllLayersTip;
    pnl.flattenAllLayers.value = CSIIDefaults.flattenLayers;

    yy += lineH;
    xx = xOfs;

    //
    // Color Mode
    //
    pnl.modeLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterR, yy + 22 + tOfs],
        ZStrings.ModeLabel);
    pnl.modeLabel.helpTip = ZStrings.ModeTip;
    xx += gutterR;

    pnl.mode = pnl.add('dropdownlist', [xx, yy, xx + menuW, yy + 22],
        [ZStrings.GrayscaleMode,
        ZStrings.RGBMode,
        ZStrings.CMYKMode,
        ZStrings.LabMode]);
    pnl.mode.helpTip = ZStrings.ModeTip;

    psxui.setMenuSelection(pnl.mode, CSIIDefaults.mode);

    pnl.mode.onChange = function () {
        var pnl = this.parent;
        var mode = pnl.mode.selection.text;
        pnl.setColorProfileMenu(mode);
    }

    pnl.RGB_PROFILES = [ZStrs.ProfileAdobeRGB,
    ZStrs.ProfileAppleRGB,
    ZStrs.ProfileProPhotoRGB,
    ZStrs.ProfileSRGB,
    ZStrs.ProfileColorMatchRGB,
    ZStrs.ProfileWideGamutRGB,
    ZStrs.ProfileWorkingRGB];

    pnl.setColorProfileMenu = function (mode) {
        var pnl = this;

        pnl.colorProfile.removeAll();
        var profiles = [];
        switch (mode) {
            case ZStrings.GrayscaleMode: profiles = [ZStrs.ProfileWorkingGray]; break;
            case ZStrings.RGBMode: profiles = pnl.RGB_PROFILES; break;
            case ZStrings.CMYKMode: profiles = [ZStrs.ProfileWorkingCMYK]; break;
            case ZStrings.LabMode: profiles = [ZStrs.ProfileLab]; break;
        }

        for (var i = 0; i < profiles.length; i++) {
            var p = profiles[i];
            pnl.colorProfile.add('item', p);
        }

        var lastProfile = pnl._lastProfile[mode];
        if (lastProfile) {
            pnl.colorProfile.selection = pnl.colorProfile.find(lastProfile);

        } else {
            pnl.colorProfile.selection = pnl.colorProfile.items[0];
            pnl._lastProfile[mode] = pnl.colorProfile.selection.text;
        }
    }


    yy += lineH;
    xx = xOfs;

    //
    // Bit Depth
    //
    pnl.bitDepthLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterR, yy + 22 + tOfs],
        ZStrings.BitDepthLabel);
    pnl.bitDepthLabel.helpTip = ZStrings.BitDepthTip;
    xx += gutterR;
    pnl.bitDepth = pnl.add('dropdownlist', [xx, yy, xx + menuW, yy + 22],
        [ZStrings.BitDepth8,
        ZStrings.BitDepth16],
        { name: 'bitDepth' });
    pnl.bitDepth.helpTip = ZStrings.BitDepthTip;
    psxui.setMenuSelection(pnl.bitDepth, CSIIDefaults.bitDepth);

    xOfs = 10;
    xx = xOfs;
    yy = yy_saved;

    //
    // Color Profile
    //
    pnl.colorProfileLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutterL, yy + 22 + tOfs],
        ZStrings.ColorProfileLabel);
    pnl.colorProfileLabel.helpTip = ZStrings.ColorProfileTip;
    xx += gutterL;

    pnl.colorProfile = pnl.add('dropdownlist', [xx, yy, xx + menuW + 35, yy + 22],
        pnl.RGB_PROFILES,
        { name: 'colorProfile' });
    pnl.colorProfile.helpTip = ZStrings.ColorProfileTip;

    // this keeps track of the last profile selected for a given mode
    pnl._lastProfile = [];
    psxui.setMenuSelection(pnl.colorProfile, CSIIDefaults.colorProfile);

    pnl.colorProfile.onChange = function () {
        var pnl = this.parent;
        var mode = pnl.mode.selection.text;
        pnl._lastProfile[mode] = pnl.colorProfile.selection.text;
    }

    pnl.mode.onChange();

    return pnl;
};

//
// Function: addNumericFilter
// Description: Adds a numeric keystroke filter to a widget
// Input:  obj - UI widget
// Return: <none>
//
ContactSheetUI.addNumericFilter = function (obj) {
    obj.keyFilter = /\d/;
    obj.addEventListener('keydown', ContactSheetUI.filterKey);
};

//
// Function: addNumberFilter
// Description: Adds a number/localized-decimal-point keystroke filter
// Input:  obj - UI widget
// Return: <none>
//
ContactSheetUI.addNumberFilter = function (obj) {
    obj.keyFilter = RegExp("[\\-" + psxui.dpREStr + "\\d]");
    obj.addEventListener('keydown', ContactSheetUI.filterKey);
};
//
// Function: addPositiveNumberFilter
// Description: Adds a positive number/localized-decimal-point keystroke filter
// Input:  obj - UI widget
// Return: <none>
//
ContactSheetUI.addPositiveNumberFilter = function (obj) {
    obj.keyFilter = RegExp("[" + psxui.dpREStr + "\\d]");
    obj.addEventListener('keydown', ContactSheetUI.filterKey);
};
//
// Function: addUnitValue
// Description: Adds a localied UnitValue keystroke filter
// Input:  obj - UI widget
// Return: <none>
//
ContactSheetUI.addUnitValueFilter = function (obj) {
    // Need to allow anything because \w doesn't work with non-latin alphabets
    return;
    obj.keyFilter = RegExp("[\w% " + psxui.dpREStr + "\\d]");
    obj.addEventListener('keydown', ContactSheetUI.filterKey);
};

//
// Function: filterKey
// Description: Keystroke event filter
// Input:  event - UI event
// Return: <none>
//
ContactSheetUI.filterKey = function (event) {
    var obj = event.currentTarget;
    var filter = obj.keyFilter;

    var c = Number("0x" + event.keyIdentifier.substring(2));
    var key = String.fromCharCode(c);

    // $.writeln(event.keyName + ' : ' + key + ' : ' + filter);

    if (filter && key.match(filter)) {
        return;

    } else if (event.keyName == 'Enter') {
        ContactSheetUI.handleEnterKey(event);

    } else if (event.keyName.length == 1) {
        event.stopPropagation();
        event.preventDefault();
    }
};

//
// Function: createCSIIThumbnailPanel
// Description: Creates the Thumbnails panel
// Input:  pnl - Panel
//         opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createCSIIThumbnailPanel = function (pnl, opts) {
    var self = this;

    var gutter = 90;
    var xOfs = 10;
    var yOfs = 12;
    var xx = xOfs;
    var yy = yOfs;
    var tOfs = self.textOfs;
    var menuW = self.menuWidth;
    var textW = self.textWidth;
    var lineH = self.lineH;

    self.thumbnailPnl = pnl;

    pnl._baseUnit = UnitValue(1 / 72, "in");

    pnl.toPixels = function (val, type) {
        val = toNumber(val) || undefined;
        var un = new UnitValue(val, type);
        un.baseUnit = pnl._baseUnit;
        return un.as('px');
    }

    //
    // Place
    //
    pnl.placeLbl = pnl.add('statictext', [xx, yy + tOfs, xx + gutter, yy + 22 + tOfs],
        ZStrings.PlaceLabel);
    pnl.placeLbl.helpTip = ZStrings.PlaceTip;
    xx += gutter;

    pnl.place = pnl.add('dropdownlist', [xx, yy, xx + menuW, yy + 22],
        [ZStrings.AcrossFirst,
        ZStrings.DownFirst],
        { name: 'place' });
    pnl.place.helpTip = ZStrings.PlaceTip;
    psxui.setMenuSelection(pnl.place, CSIIDefaults.place);

    yy += lineH;
    xx = xOfs;

    //
    // Columns
    //
    pnl.colsLbl = pnl.add('statictext', [xx, yy + tOfs, xx + gutter, yy + 22 + tOfs],
        ZStrings.ColumnsLabel);
    pnl.colsLbl.helpTip = ZStrings.ColumnsTip;
    xx += gutter;
    pnl.cols = pnl.add('editnumber', [xx, yy, xx + 50, yy + 20],
        opts.columnCount || CSIIDefaults.cols,
        ContactSheetUI.MIN_COLS_ROWS,
        ContactSheetUI.MAX_COLS_ROWS,
        { name: 'cols' });
    pnl.cols.helpTip = ZStrings.ColumnsTip;
    pnl.cols._value = CSIIDefaults.cols;

    //pnl.cols.onChanging = psxui.numericKeystrokeFilter;
    // pnl.cols.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    // we have used editnumber control for pnl.cols, so blocking of invalid input will be done internally
    //ContactSheetUI.addNumericFilter(pnl.cols);

    yy += lineH;
    xx = xOfs;

    //
    // Rows
    //
    pnl.rowsLbl = pnl.add('statictext', [xx, yy + tOfs, xx + gutter, yy + 22 + tOfs],
        ZStrings.RowsLabel);
    pnl.rowsLbl.helpTip = ZStrings.RowsTip;
    xx += gutter;
    pnl.rows = pnl.add('editnumber', [xx, yy, xx + 50, yy + 20],
        opts.rowCount || CSIIDefaults.rows,
        ContactSheetUI.MIN_COLS_ROWS,
        ContactSheetUI.MAX_COLS_ROWS,
        { name: 'rows' });
    pnl.rows.helpTip = ZStrings.RowsTip;
    pnl.rows._value = CSIIDefaults.rows;

    // pnl.rows.onChanging = psxui.numericKeystrokeFilter;
    // pnl.rows.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    ContactSheetUI.addNumericFilter(pnl.cols);
    // we have used editnumber control for pnl.cols, so blocking of invalid input will be done internally
    //ContactSheetUI.addNumericFilter(pnl.rows);

    yy += lineH;
    xx = xOfs;

    function _onChange(w, nm, min, max) {
        var pnl = w.parent;
        var n = w.value;
        w._value = n;
        return true;
    }

    pnl.cols.onChange = function () {
        return _onChange(this, ZStrings.Columns, ContactSheetUI.MIN_COLS_ROWS,
            ContactSheetUI.MAX_COLS_ROWS);
    }

    pnl.rows.onChange = function () {
        return _onChange(this, ZStrings.Rows, ContactSheetUI.MIN_COLS_ROWS,
            ContactSheetUI.MAX_COLS_ROWS);
    }

    pnl.rows.onChange();
    pnl.cols.onChange();

    //
    // Best Fit
    //
    pnl.bestFit = pnl.add('checkbox', [xx, yy, pnl.bounds.width - xx, yy + 22],
        ZStrings.RotateForBestFit,
        { name: 'bestFit' });
    pnl.bestFit.helpTip = ZStrings.RotateForBestFitTip;
    pnl.bestFit.value = CSIIDefaults.bestFit;

    //xOfs = gutter + menuW + 5 + xOfs + 50;
    xOfs = 300;
    xx = xOfs;
    yy = yOfs;
    gutter = 90;

    //
    // Autospacing
    //
    pnl.autoSpacing = pnl.add('checkbox', [xx, yy, pnl.bounds.width - xx, yy + 22],
        ZStrings.UseAutoSpacing,
        { name: 'autoSpacing' });
    pnl.autoSpacing.helpTip = ZStrings.UseAutoSpacingTip;
    pnl.autoSpacing.value = CSIIDefaults.useAutospacing;

    // See if we need to widen the panel to fit the localized string
    var d = pnl.graphics.measureString(ZStrings.UseAutoSpacing);
    var w = d[0] + 25; // add 25 for the checkbox and padding
    if (w > pnl.autoSpacing.bounds.width) {
        pnl.autoSpacing.bounds.width = w;
        pnl.bounds.width = pnl.autoSpacing.bounds.right;
    }

    yy += lineH;
    xx = xOfs;

    var units = self.currentUnits();

    textW -= 5;

    var labels = [ZStrings.VerticalLabel, ZStrings.HorizontalLabel];
    var maxW = _findMaxWidth(pnl, labels);

    gutter = Math.max(maxW + 5, gutter);

    //
    // Vertical
    //
    pnl.vertLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutter, yy + 22 + tOfs],
        ZStrings.VerticalLabel);
    pnl.vertLabel.helpTip = ZStrings.VerticalTip;
    xx += gutter;
    pnl.vert = pnl.add('edittext', [xx, yy, xx + textW, yy + 20],
        opts.vert || CSIIDefaults.vert,
        { name: 'vert' });
    pnl.vert.helpTip = ZStrings.VerticalTip;

    pnl.vert._last = pnl.toPixels(pnl.vert.text, units);

    pnl.vert.onChange = function () {
        try {
            var pnl = this.parent;
            var processor = pnl.window.processor;
            var un = processor.validateLocalizedUnitString(pnl.vert.text);

            if (!un) {
                un = processor.validateLocalizedUnitString(pnl.vert.text + ' ' + type);
            }

            var type = processor.currentUnits();

            if (!un) {
                ContactSheetUI.invalidValuePrompt(ZStrings.Vertical);
                pnl.vert.active = true;
                var v = UnitValue(pnl.vert._last, "px").as(type);
                pnl.vert.text = psx.localizeUnitValue(UnitValue(_approximate(v), type));
                return false;
            }

            var n = ContactSheetUI.getNumber(un.value.toString(),
                un.type.toString(),
                ZStrings.Vertical,
                0,
                ContactSheetUI.MAX_PIXELS,
                un.baseUnit);
            if (typeof n != "number") {
                pnl.vert.active = true;
                var v = UnitValue(pnl.vert._last, "px").as(type);
                pnl.vert.text = psx.localizeUnitValue(UnitValue(_approximate(v), type));
                return false;
            }

            var old = new UnitValue(un.value, un.type);
            un.value = n;

            pnl.vert.text = psx.localizeUnitValue(_approximate(un));
            pnl.vert._last = Math.round(un.as("px"));

            // un = UnitValue(pnl.vert.text);

            return un.value == old.value;

        } catch (e) {
            alert(psx.exceptionMessage(e));
            return false;
        }
    }

    pnl.vert.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    // ContactSheetUI.addUnitValueFilter(pnl.vert);

    yy += lineH;
    xx = xOfs;

    //
    // Horizontal
    //
    pnl.horzLabel = pnl.add('statictext',
        [xx, yy + tOfs, xx + gutter, yy + 22 + tOfs],
        ZStrings.HorizontalLabel);
    pnl.horzLabel.helpTip = ZStrings.HorizontalTip;
    xx += gutter;
    pnl.horz = pnl.add('edittext', [xx, yy, xx + textW, yy + 20],
        opts.horz || CSIIDefaults.horz,
        { name: 'horz' });
    pnl.horz.helpTip = ZStrings.HorizontalTip;

    pnl.horz._last = pnl.toPixels(pnl.horz.text, units);

    pnl.horz.onChange = function () {
        try {
            var pnl = this.parent;
            var processor = pnl.window.processor;
            var un = processor.validateLocalizedUnitString(pnl.horz.text);

            if (!un) {
                un = processor.validateLocalizedUnitString(pnl.horz.text + ' ' + type);
            }

            var type = processor.currentUnits();

            if (!un) {
                ContactSheetUI.invalidValuePrompt(ZStrings.Horizontal);
                pnl.horz.active = true;
                var v = UnitValue(pnl.horz._last, "px").as(type);
                pnl.horz.text = psx.localizeUnitValue(UnitValue(_approximate(v), type));
                return false;
            }

            var n = ContactSheetUI.getNumber(un.value.toString(),
                un.type.toString(),
                ZStrings.Horizontal,
                0,
                ContactSheetUI.MAX_PIXELS,
                un.baseUnit);
            if (typeof n != "number") {
                pnl.horz.active = true;
                var v = UnitValue(pnl.horz._last, "px").as(type);
                pnl.horz.text = psx.localieUnitValue(UnitValue(_approximate(v), type));
                return false;
            }

            var old = new UnitValue(un.value, un.type);
            un.value = n;

            pnl.horz.text = psx.localizeUnitValue(_approximate(un));
            pnl.horz._last = Math.round(un.as("px"));

            // un = UnitValue(pnl.horz.text);

            return un.value == old.value;

        } catch (e) {
            alert(psx.exceptionMessage(e));
            return false;
        }
    }

    pnl.horz.addEventListener('keydown', ContactSheetUI.handleEnterKey);
    // ContactSheetUI.addUnitValueFilter(pnl.horz);


    pnl.autoSpacing.onClick = function () {
        var pnl = this.parent;
        var st = !pnl.autoSpacing.value;

        pnl.vert.enabled = st;
        pnl.horz.enabled = st;
    }

    pnl.autoSpacing.onClick();

    return pnl;
};

ContactSheetUI.prototype.createFontPanel = psxui.createFontPanel;

//
// Function: createCSIICaptionPanel
// Description: Creates the Caption panel
// Input:  pnl - Panel
//         opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createCSIICaptionPanel = function (pnl, opts) {
    var self = this;

    var xx = 10;
    var yy = 10;

    self.captionPnl = pnl;

    pnl.captionEnabled = pnl.add('checkbox', [xx, yy - 5, xx + 26, yy + 30], '',
        { name: 'captionEnabled' });
    pnl.captionEnabled.helpTip = ZStrs.FontTip;
    pnl.captionEnabled.value = CSIIDefaults.captionEnabled;

    xx += 27;

    pnl.fontPanel = pnl.add('group', [xx, yy, pnl.bounds.width, yy + 30],
        { name: 'fontPanel' });
    self.createFontPanel(pnl.fontPanel, opts);

    var font = psx.determineFont(CSIIDefaults.fontFaceName);
    if (!font) {
        font = psx.determineFont(CSIIDefaults.fontName);
    }
    if (!font) {
        font = psx.getDefaultFont();
    }

    pnl.fontPanel.setFont(font, CSIIDefaults.fontSize);
    pnl.fontPanel._fontSize = CSIIDefaults.fontSize;

    pnl.fontPanel.fontSize.addEventListener('keydown',
        ContactSheetUI.handleEnterKey);

    pnl.fontPanel.fontSize.onChange = function () {
        var pnl = this.parent.parent;
        var fontInfo = pnl.fontPanel.getFont();
        var n = fontInfo.size;

        pnl.fontPanel._fontSize = n;
        return true;
    }

    pnl.captionEnabled.onClick = function () {
        var pnl = this.parent;
        pnl.fontPanel.enabled = pnl.captionEnabled.value;
    }

    pnl.captionEnabled.onClick();

    return pnl;
};

//
// Function: createCSIIControlPanel
// Description: Creates the Control/Button panel
// Input:  pnl - Panel
//         opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createCSIIControlPanel = function (pnl, opts) {
    var self = this;

    var w = pnl.bounds.width;
    var btnW = w - 10;
    var btnH = 25;
    var xx = 10;
    var yy = 5;


    pnl.runBtn = pnl.add('button', [xx, yy, xx + btnW, yy + btnH],
        ZStrings.OK, { name: 'ok' });
    pnl.window.defaultElement = pnl.runBtn;

    yy += btnH + 5;


    pnl.cancelBtn = pnl.add('button', [xx, yy, xx + btnW, yy + btnH],
        ZStrings.Cancel, { name: 'cancel' });
    pnl.window.cancelElement = pnl.cancelBtn;

    yy += (isWindows() ? 60 : 45);

    pnl.loadBtn = pnl.add('button', [xx, yy, xx + btnW, yy + btnH],
        ZStrings.Load, { name: 'load' });
    yy += btnH + 5;
    pnl.saveBtn = pnl.add('button', [xx, yy, xx + btnW, yy + btnH],
        ZStrings.Save, { name: 'save' });
    yy += btnH + 5;
    pnl.resetBtn = pnl.add('button', [xx, yy, xx + btnW, yy + btnH],
        ZStrings.Reset, { name: 'reset' });

    xx += 5;
    yy += (isWindows() ? 40 : 35);
    textHeight = (isWindows() ? 67 : 87);

    pnl.esc = pnl.add('statictext', [xx, yy, xx + btnW - 10, yy + textHeight], ZStrings.PressESC,
        { multiline: true });

    pnl.runBtn.onClick = function () {
        if (this.window.processor.validateCSII() == true) {
            this.window.close(1);
        }
    }

    pnl.window.runBtn = pnl.runBtn;

    // pnl.cancelBtn.onClick = function() {
    //   this.window.processor.cancel();
    // }
    pnl.resetBtn.onClick = function () {
        this.window.processor.reset();
    }
    pnl.loadBtn.onClick = function () {
        this.window.processor.load();
    }
    pnl.saveBtn.onClick = function () {
        this.window.processor.save();
    }

    return pnl;
};


//
// Function: createWindow
// Description: Creates the CSII Dialog
// Input:  ini - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.createWindow = function (ini) {
    var self = this;
    var wrect = self.winRect;

    //   $.level = 1; debugger;

    function rectToBounds(r) {
        return [r.x, r.y, r.x + r.w, r.y + r.h];
    };

    var win = new Window('dialog', "Auto-Save-to-TIFF-From-Contact-Sheet-II", rectToBounds(wrect),
        self.windowCreationProperties);


    win.mgr = self;  // save a ref to the UI manager
    win.ini = ini;
    if (!self.ini) {
        self.ini = win.ini;
    }
    self.window = self.win = win;

    var xOfs = 10;
    var yy = 10;

    var hasButtons = false;
    var hasNotesPanel = false;

    var appBottom = wrect.h - 10;

    // Now, create the application panel
    var pnlType = 'panel';
    pnlType = (self.hasBorder ? 'panel' : 'group');

    win.appPnl = win.add(pnlType, [xOfs, yy, wrect.w - xOfs, appBottom]);
    win.appPanel = win.appPnl;

    yy = appBottom + 10;

    // and call the application callback function with the ini object
    self.createPanel(win.appPnl, ini);

    return win;
};

ContactSheetUI.prototype.useDefaultUnits = function () {
};

//
// Function: execCSII_UI
// Description: Executes the CSII UI, if necessary. If opts are passed in
//              from a recorded Action, the UI may not actually run.
//              If the UI is needed, the Dialog is created and run.
//              When complete, the settings are scraped from the UI
//              and stored.
// Input:  opts - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.execCSII_UI = function (opts) {
    var self = this;

    LogFile.write("execUI");
    try {
        var win = undefined;
        var settings = opts._settings;

        if (!settings) {
            // this is (almost) identical to loadDefaultUserOptions
            var file = ContactSheetUI.getUserOptionsFile();
            if (file.exists) {
                settings = psx.readXMLFile(file);

                if (!ContactSheetUI.isValidLocale(settings)) {
                    LogFile.write("locale mismatch:" + $.locale + " - " +
                        settings.locale.toString());
                    settings = ContactSheetUI.getDefaultOptionsObj();
                } else {
                    opts._settings = settings;
                }
            } else {
                settings = ContactSheetUI.getDefaultOptions();
            }
        }

        if (!toBoolean(opts.noUI)) {
            win = self.createWindow(opts);
            self.win = win;
            self.writeSettings(settings);

            // if we are using default settings,
            // we need to tweak the default units setting to use
            // the Ruler Units preference
            if (!opts._settings) {
                self.setDefaultUnits();
            }

            win.center();
            var res = win.show();

            if (res == 1) {
                var xml = self.readSettings();
                self.settings = xml;

                if (!opts._runFromAction) {
                    self.saveDefaultUserOptions(xml);
                }
            } else {
                self.settings = undefined;
                opts = undefined;
                gScriptResult = 'cancel';
            }

        } else {
            // No UI. Just get the settings...
            self.settings = settings;
        }
    } catch (e) {
        var msg = psx.exceptionMessage(e);
        LogFile.write(msg);

        if (confirm(ZStrings.ErrorDetailsPrompt)) {
            alert(msg);
        }
    }

    return opts;
};

//
// Function: process
// Description: Converts the XML settings and calls the Contact Sheet processor
// Input:  ini - ContactSheetIIOptions
// Return: pnl
//
ContactSheetUI.prototype.process = function (ini) {
    var self = this;
    var xml = self.settings;

    if (xml == undefined) {
        Error.runtimeError(9002, ZStrings.UnknownInternalError +
            " - No settings available");
    }

    // switch log files at this point...
    LogFile.setFilename(ContactSheetIIOptions.LOG_FILE, "UTF8");
    LogFile.write(xml.toXMLString());

    // we control the log file, not CSXCore
    ContactSheetIIOptions.LOG_ENABLED = false;

    LogFile.write("process");

    var opts = new ContactSheetIIOptions(ini);

    ContactSheetUI.settingsToOpts(xml, opts);
    // var s = psx.toIniString(opts);
    // opts = psx.fromIniString(s);

    opts.files = self._fileList;

    LogFile.write(psx.listProps(opts));

    var csx = self.createContactSheetII();

    var res = csx.process(opts);

    self.descriptor = csx.descriptor;

    return opts;
};

//
// Function: settingsToOpts
// Description: Converts the XML settings to ContactSheetIIOptions
// Input:  settings - ContactSheetIIOptions
//         opts - ContactSheetIIOptions
// Return: opts
//
ContactSheetUI.settingsToOpts = function (settings, opts) {
    if (!opts) {
        opts = new ContactSheetIIOptions();
    }

    // Source
    opts.imageSource = settings.source.@imageSource.toString();
    opts.folder = settings.source.@path.toString();
    opts.recurse = toBoolean(settings.source.@includeSubfolders);

    if (opts.imageSource == ZStrings.Folder) {
        opts.spanFolders = !toBoolean(settings.source.@groupImages);
    } else {
        opts.spanFolders = true;
    }

    // Document
    opts.background = 'White';
    var str = settings.document.@resUnits.toString().toLowerCase();
    var resType = (str == ZStrings.PixelsPerInch.toLowerCase()) ? "in" : "cm";

    var res = psx.delocalizeNumber(settings.document.@resolution);
    opts.resolution = (resType == "cm") ? _approximate(res * 2.54) : res;

    opts.resUnits = "in";

    var baseUnit = new UnitValue(1 / opts.resolution, opts.resUnits);

    opts.docUnits = 'pixels';
    var units = delocalizeUnitType(settings.document.@units.toString());

    var un = new UnitValue(psx.delocalizeNumber(settings.document.@width), units);
    un.baseUnit = baseUnit;
    opts.docWidth = un.as('px');

    var un = new UnitValue(psx.delocalizeNumber(settings.document.@height), units);
    un.baseUnit = baseUnit;
    opts.docHeight = un.as('px');

    opts.mode = ContactSheetIIOptions.toModeStr(settings.document.@mode.toString());
    opts.bitDepth = settings.document.@bitDepth.toString();

    var str = settings.document.@colorProfile.toString();
    // XXX localization hack
    var m = str.match(/.+\[.+\] (.+)/);
    var profile = (m ? m[1] : str);
    opts.colorProfile = psx.delocalizeProfile(profile);
    opts.marginsEnabled = false;

    // Thumbnail
    opts.acrossFirst = settings.thumbnail.@place.toString() != ZStrings.DownFirst;
    opts.bestFit = toBoolean(settings.thumbnail.@bestFit);
    opts.rotate = 'CCW';
    opts.autoFill = false;
    opts.rowCount = toNumber(settings.thumbnail.@rows);
    opts.columnCount = toNumber(settings.thumbnail.@cols);

    opts.useAutoSpacing = toBoolean(settings.thumbnail.@useAutospacing);

    var baseUnit = new UnitValue(1 / opts.resolution, resType);
    var un = new UnitValue(settings.thumbnail.@vert);
    un.baseUnit = baseUnit;
    opts.vertical = un.as('px') / 2;

    var un = new UnitValue(settings.thumbnail.@horz);
    un.baseUnit = baseUnit;
    opts.horizontal = un.as('px') / 2;

    opts.spacingUnits = 'px'

    opts.caption = toBoolean(settings.caption.@enabled);
    if (opts.caption) {
        if (settings.caption.font != undefined) {
            var font = settings.caption.font.@name.toString();
            opts.font = font;
            opts.fontSize = toNumber(settings.caption.font.@size);
        }
    }

    opts.keepOpen = true;
    opts.saveSheet = false;
    opts.flatten = toBoolean(settings.document.@flattenLayers);
    opts.flattenCells = false;
    opts.fileNameFormat = ZStrings.ContactSheetFilenameFormat;
    opts.prefix = opts.fileNameFormat.replace(/#/g, '');


    opts.metadataSubstitution = false;
    opts.accumulateKeywords = false;
    opts.enableTextSubstitution = false;
    opts.forceRecording = true;
    opts.fixMaskPhantomPixels = false;

    opts.csiiMode = true;

    return opts;
};

//
// Function: validateCSII
// Description: Scrape the settings from the UI and validate where appropriate
// Input:  <none>
// Return: true if OK, false if there was a problem
//
ContactSheetUI.prototype.validateCSII = function () {
    var self = this;

    LogFile.write("validateCSII");

    try {
        // read settings from the UI
        var xml = self.readSettings();

        // validate settings
        var src = xml.source;
        if (src.@imageSource == ZStrings.Folder && src.@path == "") {
            alert(ZStrings.SelectAnImageFolderWarning);
            return false;
        }

        if (self._fileList && self._fileList.length == 0) {
            alert(ZStrings.SelectImageFilesWarning);
            return false;
        }

        // check (roughly) to see if any numbers are really out of whack
        var width = self.docPnl.width._last;
        var height = self.docPnl.height._last;
        var rez = self.docPnl.resolution._value;
        var cols = self.thumbnailPnl.cols.value;
        var rows = self.thumbnailPnl.rows.value;

        var vert = 0;
        var horz = 0;

        if (!toBoolean(xml.thumbnail.@useAutospacing)) {
            vert = self.thumbnailPnl.vert._last;
            horz = self.thumbnailPnl.horz._last;

        } else {
            vert = (width / cols) / 100 * 2;
            horz = (height / rows) / 100 * 2;
        }

        var fontSize = 0;
        if (toBoolean(xml.caption.@enabled)) {
            fontSize = toNumber(xml.caption.font.@size);
        }

        var cellWidth = Math.floor(width / cols);
        var imageWidth = cellWidth - horz;
        var cellHeight = Math.floor(height / rows);
        var imageHeight = cellHeight - vert - fontSize;

        if (imageWidth < 0 || imageHeight < 0) {
            alert(ZStrings.InvalidThumbnailSize);
            return false;
        }

        self.settings = xml;

    } catch (e) {
        LogFile.logException(e, ZStrings.ReadSettingsError + e.message, true);
        return false;
    }

    return true;
};


//
// Function: load
// Description: Prompt the user to select an XML settings file to load.
//              If a file is selected, the UI is updated.
// Input:  <none>
// Return: <none>
//
ContactSheetUI.prototype.load = function () {
    var self = this;

    LogFile.write("ContactSheetUI.load()");

    try {
        var file = psx.selectFileOpen(ZStrings.LoadSettingsFile,
            ZStrings.XMLFileDlgPattern,
            ContactSheetUI.getUserOptionsFile());
        if (file) {
            var xml = psx.readXMLFile(file);
            self.writeSettings(xml);
        }

    } catch (e) {
        alert(psx.exceptionMessage(e));
    }
};

//
// Function: save
// Description: Prompt the user to select an XML settings file to save.
//              If a file is selected, the current settings are saved.
// Input:  <none>
// Return: <none>
//
ContactSheetUI.prototype.save = function () {
    var self = this;

    LogFile.write("ContactSheetUI.save()");

    try {
        var file = psx.selectFileSave(ZStrings.SaveSettingsFile,
            ZStrings.XMLFileDlgPattern,
            ContactSheetUI.getUserOptionsFile());

        if (file) {
            var xml = self.readSettings();
            xml.date = new Date().toISOString();
            xml.version = CSII.RELEASE;
            xml.locale = $.locale;
            psx.writeXMLFile(file, xml);
        }

    } catch (e) {
        alert(psx.exceptionMessage(e));
    }
};

//
// Function: reset
// Description: Asks the user if they want to reset the UI.
//              If yes, the UI is reset.
// Input:  <none>
// Return: <none>
//
ContactSheetUI.prototype.reset = function () {
    var self = this;

    LogFile.write("ContactSheetUI.reset()");

    if (confirm(ZStrings.ConfirmReset)) {
        var xml = ContactSheetUI.getDefaultOptions();
        self.writeSettings(xml);
        self.setDefaultUnits();
    }
};

//
// Function: setDefaultUnits
// Description: Sets the default Unit type in the UI.
// Input:  <none>
// Return: <none>
//
ContactSheetUI.prototype.setDefaultUnits = function () {
    var self = this;
    var type = undefined;
    var resType = undefined;
    var str = psx.getDefaultRulerUnitsString();
    var metric = ($.locale != "en_US");

    if (str == "in") {
        type = ZStrings.Inches;
        resType = ZStrings.PixelsPerInch;

    } else if (str == "px") {
        type = (metric ? ZStrings.Centimeter : ZStrings.Inches);
        resType = ((!metric || type == ZStrings.Inches) ?
            ZStrings.PixelsPerInch : ZStrings.PixelsPerCM);

    } else if (metric || str == "cm" || str == "mm") {
        type = ZStrings.Centimeter;
        resType = ZStrings.PixelsPerCM;

    } else {
        type = ZStrings.Inches;
        resType = ZStrings.PixelsPerInch;
    }

    psxui.setMenuSelection(self.docPnl.units, type, undefined, true);
    psxui.setMenuSelection(self.docPnl.resUnits, resType, undefined, true);
};


//
// Function: readSettings
// Description: Reads the settings from the UI
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.prototype.readSettings = function () {
    var self = this;
    var settings = ContactSheetIIOptions.DEFAULT_XML.copy();

    settings.source = self.readSourceSettings();
    settings.document = self.readDocumentSettings();
    settings.thumbnail = self.readThumbnailSettings();
    settings.caption = self.readCaptionSettings();

    return settings;
};

//
// Function: writeSettings
// Description: Writes XML settings to the UI
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.writeSettings = function (settings) {
    var self = this;

    self.writeSourceSettings(settings.source);
    self.writeDocumentSettings(settings.document);
    self.writeThumbnailSettings(settings.thumbnail);
    self.writeCaptionSettings(settings.caption);
};

//
// Function: readSourceSettings
// Description: Reads the settings from the Document panel
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.prototype.readSourceSettings = function () {
    var self = this;
    var pnl = self.sourcePanel;
    var settings = new XML("<source/>");

    var src = pnl.imageSource.selection.text;
    settings.@imageSource = src;

    self._fileList = undefined;
    if (src == ZStrings.Files) {
        self._fileList = self.sourcePanel.fileSet;
    }

    if (src == ZStrings.Bridge) {
        self._fileList = [];
        var files = ContactSheetUI.getImageFilesFromBridge();
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            self._fileList.push(file);
        }
    }

    var s = pnl.folder.text;
    if (s == ZStrings.NoFolderSelected) {
        s = "";

    } else {
        s = pnl.folder._folder.toUIString();
    }
    settings.@path = s;
    settings.@includeSubfolders = pnl.recurse.value;
    settings.@groupImages = pnl.group.value;

    return settings;
};

//
// Function: writeSourceSettings
// Description: Writes XML settings to the Source panel
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.writeSourceSettings = function (settings) {
    var self = this;
    var pnl = self.sourcePanel;

    if (ContactSheetII.runningFromBridge) {
        psxui.setMenuSelection(pnl.imageSource, ZStrings.Bridge);

    } else {
        var imageSource = settings.@imageSource.toString();
        psxui.setMenuSelection(pnl.imageSource,
            imageSource || CSIIDefaults.imageSource,
            CSIIDefaults.imageSource);
    }

    var path = ZStrings.NoFolderSelected;
    if (settings.@path != undefined && settings.@path.toString() != "") {
        path = settings.@path.toString();
    }
    if (path != ZStrings.NoFolderSelected) {
        pnl.folder._folder = Folder(path);
    } else {
        pnl.folder._folder = undefined;
    }
    pnl.folder.text = ContactSheetUI.shortFileName(path);

    pnl.recurse.value = toBoolean(settings.@includeSubfolders);
    pnl.group.value = toBoolean(settings.@groupImages);

    pnl.imageSource.onChange();
    pnl.recurse.onClick();
};


//
// Function: readDocumentSettings
// Description: Reads the settings from the Document panel
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.prototype.readDocumentSettings = function () {
    var self = this;
    var pnl = self.docPnl;
    var settings = new XML("<document/>");

    settings.@units = pnl.units.selection.text;
    settings.@width = pnl.width.value;
    settings.@height = pnl.height.value;
    settings.@resolution = pnl.resolution.value;
    settings.@resUnits = pnl.resUnits.selection.text;

    settings.@mode = pnl.mode.selection.text;
    settings.@bitDepth = pnl.bitDepth.selection.text;

    settings.@colorProfile = pnl.colorProfile.selection.text;
    settings.@flattenLayers = pnl.flattenAllLayers.value;

    return settings;
};

ContactSheetUI._getXMLValue = function (xml, def) {
    return (xml == undefined) ? def : xml.toString();
}

//
// Function: writeDocumentSettings
// Description: Writes XML settings to the Document panel
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.writeDocumentSettings = function (settings) {
    var self = this;
    var pnl = self.docPnl;

    var str = settings.@units.toString();

    psxui.setMenuSelection(pnl.units, str || CSIIDefaults.units,
        undefined, true);

    pnl.width.value = psx.getXMLValue(settings.@width, CSIIDefaults.width);
    pnl.height.value = psx.getXMLValue(settings.@height, CSIIDefaults.height);

    // we have set the resUnits before setting the resolution
    // to avoid a conversion problem
    var str = settings.@resUnits.toString();
    psxui.setMenuSelection(pnl.resUnits, str || CSIIDefaults.resUnits,
        undefined, true);

    pnl.resolution.value = psx.getXMLValue(settings.@resolution,
        CSIIDefaults.resolution);

    psxui.setMenuSelection(pnl.mode,
        psx.getXMLValue(settings.@mode, CSIIDefaults.mode));

    psxui.setMenuSelection(pnl.bitDepth,
        psx.getXMLValue(settings.@bitDepth,
            CSIIDefaults.bitDepth));

    var str = CSIIDefaults.colorProfile;
    if (settings.@colorProfile != undefined) {
        var str = settings.@colorProfile.toString();
        // XXX localization hack
        var m = str.match(/.+\[.+\] (.+)/);
        str = (m ? m[1] : str);
    }

    psxui.setMenuSelection(pnl.colorProfile, str);

    pnl.flattenAllLayers.value = toBoolean(settings.@flattenLayers);

    pnl.resolution.onChange();
    pnl.width.onChange();
    pnl.height.onChange();
};


//
// Function: readThumbnailSettings
// Description: Reads the settings from the Thumbnails panel
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.prototype.readThumbnailSettings = function () {
    var self = this;
    var pnl = self.thumbnailPnl;
    var settings = new XML("<thumbnail/>");

    settings.@place = pnl.place.selection.text;
    settings.@cols = pnl.cols.value;
    settings.@rows = pnl.rows.value;
    settings.@bestFit = pnl.bestFit.value;

    settings.@useAutospacing = pnl.autoSpacing.value;

    // XXX may need to append a unit type on these
    settings.@horz = psx.delocalizeNumber(pnl.horz.text);
    settings.@vert = psx.delocalizeNumber(pnl.vert.text);

    return settings;
};

ContactSheetUI.prototype.validateLocalizedUnitString = function (s, defValueStr) {
    var self = this;
    var units = self.currentUnits();

    if (isString(s)) {
        s = psx.delocalizeNumber(s);
        var ar = s.split(/\s+/);
        if (ar.length == 2) {
            var type = delocalizeUnitType(ar[1]);
            // this bit of code lets us parse EN as well
            if (type) {
                ar[1] = type;
            }
        }
        s = ar.join(' ');
    }
    var un = new UnitValue(s);

    if (isNaN(un.value) || un.type == '?') {
        var n = toNumber(s);
        if (isNaN(n)) {
            if (defValueStr) {
                un = UnitValue(defValueStr);
            } else {
                un = undefined;
            }
        } else {
            un = UnitValue(s + ' ' + units);
        }
        if (un && un.type == '?') {
            un = undefined;
        }
    }

    if (un) {
        un.convert(units);
    }

    return un;
};

//
// Function: writeThumbnailSettings
// Description: Writes XML settings to the Thumbnails panel
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.writeThumbnailSettings = function (settings) {
    var self = this;
    var pnl = self.thumbnailPnl;

    var str = settings.@place.toString();
    psxui.setMenuSelection(pnl.place, str || CSIIDefaults.place);

    pnl.cols.value = psx.getXMLValue(settings.@cols, CSIIDefaults.cols);
    pnl.rows.value = psx.getXMLValue(settings.@rows, CSIIDefaults.rows);
    pnl.bestFit.value = toBoolean(settings.@bestFit);

    pnl.autoSpacing.value = toBoolean(settings.@useAutospacing);

    // may need to add some error handling in here...
    var un = self.validateLocalizedUnitString(settings.@vert.toString(),
        CSIIDefaults.vert + " " + ZStrs.UnitsIN);
    pnl.vert.text = _approximate(un).toString();

    pnl.vert.onChange();

    var un = self.validateLocalizedUnitString(settings.@horz.toString(),
        CSIIDefaults.horz + " " + ZStrs.UnitsIN);
    pnl.horz.text = _approximate(un).toString();
    pnl.horz.onChange();

    pnl.autoSpacing.onClick();
};

//
// Function: readCaptionSettings
// Description: Reads the settings from the Caption panel
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.prototype.readCaptionSettings = function () {
    var self = this;
    var pnl = self.captionPnl;
    var settings = new XML("<caption/>");

    settings.@enabled = pnl.captionEnabled.value;

    try {
        var font = pnl.fontPanel.getFont();
        settings.font.@name = font.font;
        settings.font.@size = font.size;
    } catch (e) {
    }

    return settings;
};

//
// Function: writeCaptionSettings
// Description: Writes XML settings to the Caption panel
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.writeCaptionSettings = function (settings) {
    var self = this;
    var pnl = self.captionPnl;

    pnl.captionEnabled.value = toBoolean(settings.@enabled);

    try {
        var size = toNumber(settings.font.@size) || CSIIDefaults.fontSize;

        // if we don't have a good font name in the settings,
        // try to find a reasonable default for this locale
        var name = settings.font.@name.toString();
        var font = psx.determineFont(name);

        if (!font) {
            font = psx.determineFont(CSIIDefaults.fontFaceName);
        }
        if (!font) {
            font = psx.determineFont(CSIIDefaults.fontName);
        }
        if (!font) {
            font = psx.getDefaultFont();
        }

        name = font.name;

        pnl.fontPanel.setFont(name, size);

    } catch (e) {
    }

    pnl.captionEnabled.onClick();
};

//======================================================================

//
// Function: getUserOptionsFile
// Description: Gets the users XML settings file
// Input:  <none>
// Return: XML settings file
//
ContactSheetUI.getUserOptionsFile = function () {
    return File(ContactSheetIIOptions.SETTINGS_FOLDER + '/' +
        ContactSheetIIOptions.SETTINGS_FILE_NAME);
};

//
// Function: getDefaultOptionsObj
// Description: Gets the default XML settings
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.getDefaultOptionsObj = function () {
    var xml = ContactSheetIIOptions.DEFAULT_XML.copy();
    xml.source = XML("<source/>");
    xml.source.@imageSource = CSIIDefaults.imageSource;
    xml.source.@path = CSIIDefaults.path;
    xml.source.@includeSubfolders = CSIIDefaults.includeSubfolders;
    xml.source.@groupImages = CSIIDefaults.groupImages;

    xml.document = XML("<document/>");
    xml.document.@units = CSIIDefaults.units;
    xml.document.@width = CSIIDefaults.width;
    xml.document.@height = CSIIDefaults.height;
    xml.document.@resolution = CSIIDefaults.resolution;
    xml.document.@resUnits = CSIIDefaults.resUnits;
    xml.document.@mode = CSIIDefaults.mode;
    xml.document.@bidDepth = CSIIDefaults.bitDepth;
    xml.document.@colorProfile = CSIIDefaults.colorProfile;
    xml.document.@flattenLayers = CSIIDefaults.flattenLayers;

    xml.thumbnail = XML("<thumbnail/>");
    xml.thumbnail.@place = CSIIDefaults.place;
    xml.thumbnail.@cols = CSIIDefaults.cols;
    xml.thumbnail.@rows = CSIIDefaults.rows;
    xml.thumbnail.@bestFit = CSIIDefaults.bestFit;
    xml.thumbnail.@useAutospacing = CSIIDefaults.useAutospacing;
    xml.thumbnail.@vert = CSIIDefaults.vert;
    xml.thumbnail.@horz = CSIIDefaults.horz;

    xml.caption = XML("<caption/>");
    xml.caption.@enabled = CSIIDefaults.captionEnabled;
    xml.caption.font = XML("<font/>");
    xml.caption.font.@fontName = CSIIDefaults.fontName;
    xml.caption.font.@fontSize = CSIIDefaults.fontSize;

    return xml;
};

//
// Function: isValidLocale
// Description: Determines if the locale of the XML settings match
//              the current locale
// Input:  XML settings
// Return: true if the locale matches, false if not
//
ContactSheetUI.isValidLocale = function (xml) {
    if (xml.locale != undefined) {
        var l = xml.locale.toString();
        return $.locale.startsWith(l.substring(0, 2))
    }

    return true;
};

//
// Function: getDefaultOptions
// Description: Loads the XML settings from the user's default settings file.
//              If no file is present, hardwired defaults are used.
// Input:  <none>
// Return: XML settings
//
ContactSheetUI.getDefaultOptions = function () {
    var xml = undefined;
    var f = ContactSheetUI.getDefaultOptionsFile();

    if (f.exists) {
        xml = psx.readXMLFile(f);

        if (!ContactSheetUI.isValidLocale(xml)) {
            LogFile.write("locale mismatch:" + $.locale + " - " + xml.locale.toString());
            xml = ContactSheetUI.getDefaultOptionsObj();
        }

    } else {
        xml = ContactSheetUI.getDefaultOptionsObj();
    }

    return xml;
};

//
// Function: getDefaultOptionsFile
// Description: Attempts to find settings file in the Scripts folder
//              or where CSII is located. This would function as
//              site specific defaults.
// Input:  <none>
// Return: XML settings file
//
ContactSheetUI.getDefaultOptionsFile = function () {
    var f = File($.fileName);

    if (f.exists) {
        f = File(f.parent + '/' + ContactSheetIIOptions.SETTINGS_FILE_NAME);
    }

    if (!f.exists) {
        var strScripts = ZStrs.InstalledScripts;
        var strProcessXML = ContactSheetIIOptions.SETTINGS_FILE_NAME;
        f = new File(app.path + '/' + strScripts + "/" + strProcessXML);
    }

    return f;
};


//
// Function: readDefaultSettings
// Description: Attempts to read a site-specific default XML settings file
// Input:  <none>
// Return: XML settings or undefined
//
ContactSheetUI.readDefaultSettings = function () {
    var file = ContactSheetUI.getDefaultOptionsFile();
    return (file.exists) ? psx.readXMLFile(file) : undefined;
};

//
// Function: loadDefaultUserOptions
// Description: Attempts to read a user's default XML settings file
// Input:  <none>
// Return: XML settings or undefined
//
ContactSheetUI.prototype.loadDefaultUserOptions = function () {
    var self = this;
    var file = ContactSheetUI.getUserOptionsFile();
    var settings = undefined;

    if (file.exists) {
        settings = psx.readXMLFile(file);

        if (!ContactSheetUI.isValidLocale(settings)) {
            LogFile.write("locale mismatch:" + $.locale + " - " +
                settings.locale.toString());
            settings = ContactSheetUI.getDefaultOptionsObj();
        }
    } else {
        settings = ContactSheetUI.getDefaultOptions();
    }
    return settings;
};

//
// Function: saveDefaultUserOptions
// Description: Writes settings to a user's default XML settings file
// Input:  XML settings
// Return: <none>
//
ContactSheetUI.prototype.saveDefaultUserOptions = function (settings) {
    var self = this;
    var file = ContactSheetUI.getUserOptionsFile();
    var xml = settings.copy();
    xml.date = new Date().toISOString();
    xml.version = CSII.RELEASE;
    xml.locale = $.locale;
    psx.writeXMLFile(file, xml);
};


//
// Function: findWidgets
// Description: Populates an object with widget references
//              using the name as the key
// Input:  c - A widget container
//         o - A object to contain widgets
//         recurse - search into nested containers
// Return: <none>
//
ContactSheetUI.findWidgets = function (c, o, recurse) {
    if (o == undefined) {
        o = {};
    }

    if (!ContactSheetUI.isContainer(c)) {
        return o;
    }

    if (recurse == undefined) {
        recurse = true;
    }

    var children = c.children;

    if (children == undefined || children.length == 0) {
        return o;
    }

    var len = children.length;

    for (var i = 0; i < len; i++) {
        var p = children[i];
        if (p == undefined) {
            continue;
        }

        if (ContactSheetUI.isContainer(p) && recurse == true) {
            ContactSheetUI.findWidgets(p, o, recurse);
        }

        if (p.properties != undefined && p.properties.name != undefined) {
            o[p.properties.name] = p;
        }
    }

    return o;
};

//
// Function: isContainer
// Description: Determines if a UI widget is a container
// Input:  o - A UI widget
// Return: true if is a container, false if not
//
ContactSheetUI.isContainer = function (o) {
    return (o.type == 'dialog' || o.type == 'group' ||
        o.type == 'panel' || o.type == 'tab');
};

//
// Function: attrsToIni
// Description: Populates an object with the attributes of an XML node
// Input:  XML settings
// Return: an object
//
ContactSheetUI.attrsToIni = function (xml) {
    var ini = {};
    var attrs = xml.attributes();
    var len = attrs.length();
    for (var i = 0; i < len; i++) {
        var attr = attrs[i];
        ini[attr.localName()] = attr.toString();
    }
    return ini;
};


//
// Function: shortFileName
// Description: Returns a shortend version of a file name for the UI
// Input:  fname - File or String
// Return: a String
//
ContactSheetUI.shortFileName = function (fname) {
    if (fname instanceof File) {
        fname = fname.toUIString();
    }
    var max = ContactSheetIIOptions.SHORT_FILENAME_LENGTH;
    if (fname.length > max) {
        fname = "..." + fname.substr(fname.length - max + 3, max - 3);
    }
    return fname;
};


//
// Function: getFilesFromBridge
// Description: Gets the currently selected files in Bridge
// Input:  <none>
// Return: Array of Files
//
ContactSheetUI.getFilesFromBridge = function () {
    var fileList;
    if (BridgeTalk.isRunning("bridge")) {
        var bt = new BridgeTalk();
        bt.target = "bridge";
        bt.body =
            "var lst = photoshop.getBridgeFileListForAutomateCommand(true, false);" +
            "lst.toSource();";
        bt.onResult = function (inBT) { fileList = eval(inBT.body); }
        bt.onError = function (inBT) { fileList = new Array(); }
        bt.send();
        bt.pump();
        $.sleep(100);
        var timeOutAt = (new Date()).getTime() + 5000;
        var currentTime = (new Date()).getTime();
        while ((currentTime < timeOutAt) && (undefined == fileList)) {
            bt.pump();
            $.sleep(100);
            currentTime = (new Date()).getTime();
        }
    }

    if (undefined == fileList) {
        fileList = new Array();
    }

    return fileList;
};

//
// Function: getImageFilesFromBridge
// Description: Gets the currently selected image files in Bridge
// Input:  <none>
// Return: Array of Files
//
ContactSheetUI.getImageFilesFromBridge = function () {
    var list = ContactSheetUI.getFilesFromBridge();
    var fileList = [];

    for (var i = 0; i < list.length; i++) {
        var file = list[i];
        if (psx.isValidImageFile(file)) {
            fileList.push(file);
        }
    }

    return fileList;
};


//
// Function: photoshopFileOpenDialog
// Description: Prompts the user for one or more files
// Input:  <none>
// Return: Array of filenames
//
function photoshopFileOpenDialog() {
    var result = [];
    var nlist = app.openDialog();
    var isWin = $.os.match(/^Windows.*/);

    for (var i = 0; i < nlist.length; i++) {
        var s = decodeURI(nlist[i].toString());
        s = s.replace(/^file:\/\//, "");
        if (isWin) {  // Pull off ":" from drive letter
            s = s.replace(/^\/(.):\//, "/$1/");
        }
        result.push(s);
    }

    return result;
};

//
// Function: delocalizeUnitType
// Description: Convert a localized unit type string to something that
//              can be used to construct a UnitValue object.
// Input: A localized unit type strings
// Return: The EN form of the string
//
function delocalizeUnitType(localTxt) {
    var txt = psx.delocalizeUnitType(localTxt);

    // if we found it, return it
    if (txt) {
        return txt;
    }

    // It might be one of the CSII UI strings
    // which we handle here
    switch (localTxt) {
        case ZStrings.Centimeter: txt = "cm"; break;
        case ZStrings.Inches: txt = "in"; break;
        case ZStrings.Pixels: txt = "px"; break;
        case ZStrings.PixelsPerInch: txt = "pixels/in"; break;
        case ZStrings.PixelsPerCM: txt = "pixels/cm"; break;
    }

    return txt;

    // may need to be this
    // return txt || localTxt;
};

"CSIIExt.jsx";
// EOF

//

// This is where we start
ContactSheetII.main = function () {
    CSII.REVISION = "$Revision: 1.8 $";

    if (!ContactSheetII.isCompatible()) {
        alert(ZStrings.VersionWarning.sprintf(CSII.REQUIRED_PS_VERSION));
        return;
    }

    // set the log file for the UI portion of the script

    // LogFile.setFilename(ContactSheetIIOptions.UI_LOG_FILE, "UTF8");
    LogFile.setFilename(undefined);
    LogFile.write("Version: " + CSII.VERSION);
    LogFile.write("Revision: $Revision: 1.8 $");
    LogFile.write("App: " + app.name);
    LogFile.write("App Version: " + app.version);
    LogFile.write("OS: " + $.os);

    LogFile.write("CSII Starting up.");
    ContactSheetII.dialogMode = app.displayDialogs;
    app.displayDialogs = DialogModes.NO;

    try {
        ContactSheetII._main();

    } catch (e) {
        LogFile.logException(e, "", true);

    } finally {
        app.displayDialogs = ContactSheetII.dialogMode;
        LogFile.write("ContactSheetII Shutting down.");
    }
};


//
// Function: _main
// Usage: Determines the context in which CSII is being invoked
//        This context may be a) from File > Automate, from Bridge,
//        or from a recorded Action.
// Input:  <none>
// Return: <none>
//
ContactSheetII._main = function () {

    // This start up logic may not cover everything, but it's good enough
    // for now...
    //
    // For displayDialogs
    // ALL - direct exec (menu/browse) or from action with dialogs
    // ERROR - from action no dialogs
    // NO - from debugger
    //
    var mode = app.playbackDisplayDialogs;

    // if we are launched from the debugger OR we have no parameters
    // we need a UI
    if (mode == DialogModes.NO || app.playbackParameters.count == 0) {
        mode = DialogModes.ALL;
    }

    // only turn off the ui if we have parameters and are called from an action
    var opts = {
        noUI: (app.playbackDisplayDialogs == DialogModes.ERROR)
    };

    // This code handles the CSII being called via the
    // Automation Framework in CS3+
    var lvl = $.level;
    $.level = 0;

    // Get things setup for a possible call from Bridge
    try {
        ContactSheetII.runningFromBridge = false;
        ContactSheetII.filesFromBridge = undefined;

        // see if we are called by the automation framework
        if (app.playbackParameters.hasKey(CSII.kFilesList)) {
            ContactSheetII.runningFromBridge = true;
            ContactSheetII.filesFromBridge = [];
            opts.noUI = false;

            var flist = app.playbackParameters.getList(CSII.kFilesList);
            for (var i = 0; i < flist.count; i++) {
                ContactSheetII.filesFromBridge.push(flist.getPath(i));
            }

            // since we are only interested in the filesList
            // we can clear the playbackParameters and read in
            // settings from the user's or default XML file
            app.playbackParameters = new ActionDescriptor();
            app.displayDialogs = DialogModes.ERROR;
        }

        ContactSheetII.bridgeIsRunning = BridgeTalk.isRunning("bridge");

    } catch (e) {
    }
    $.level = lvl;

    var desc;
    if (app.playbackParameters.count > 0) {
        // now set up our options based on the parameters
        // for use with Adobe Automation framework. This will
        // typically be when called from an Action

        // Start off with the default hardwired settings
        opts = new ContactSheetIIOptions(opts);

        // Get the recorded settings, if available
        var desc = app.playbackParameters;
        var xml = ContactSheetII.fromDescriptor(desc);

        // If no settings were available, use the default settings
        if (xml == undefined) {
            xml = ContactSheetUI.readDefaultSettings();
            opts.noUI = false;
        }

        opts._settings = xml;
        opts._runFromAction = true;

        // If CSII was recorded using Files as the source, force
        // make sure the UI is used.
        if (xml.source.@imageSource.toString() == ZStrings.Files) {
            opts.noUI = false;
        }

        //     var dstr = app.playbackParameters.toStream();
        //     psx.writeToFile("/c/work/csx/in-desc.bin", dstr, 'BINARY');

    } else {
        opts.noUI = false;
        opts._runFromAction = false;
    }

    // Uncomment the next line to force CSII to run without a UI 
    // opts.noUI = true;

    if (!opts.noUI) {
        app.bringToFront();
    }

    var ui = new ContactSheetUI(opts);

    // Overrides for CSII
    ui.iniFile = undefined;
    ui.windowCreationProperties.resizeable = false;

    // Run the UI here...
    try {
        var uiOpts = ui.execCSII_UI(opts);

    } catch (e) {
        LogFile.logException(e, ZStrings.ContactSheetIIUIError, true);
        return;
    }

    // No uiOpts means the user canceled the script
    if (!uiOpts) {
        return;
    }

    // Do the processing here...
    try {
        var xopts = ui.process(uiOpts);

    } catch (e) {
        LogFile.logException(e, ZStrings.ContactSheetIIProcessingError, true);
        return;
    }

    // If the script completes successfully, save the settings
    // as a descriptor that can be recorded
    if (xopts) {
        try {
            // see if 'forceRecording' is set to 'true'
            if (toBoolean(xopts.forceRecording) && ui.descriptor) {
                var xdesc = ui.descriptor;
                var dstr = xdesc.toStream();

                //     psx.writeToFile("/c/work/csx/out-desc.bin", dstr, 'BINARY');

                var desc;

                desc = new ActionDescriptor();
                desc.fromStream(dstr);
                desc.putString(CSII.kMessage, ZStrings.SettingsForCSII);

                app.playbackParameters = desc;
            }

        } catch (e) {
            LogFile.logException(e, ZStrings.ContactSheetIISettingsError, true);
        }
    }
};

var gScriptResult = '';

// Let's get this party started...

////////// Original Script Extended 2021 - Stephen Marsh ////////// 
// Select the output folder
var theOutputFolder = Folder.selectDialog('Select folder to use as the base filename and save location...\n(if you press cancel, the contact sheets will not auto name, save or close!)');
////////// Original Script Extended 2021 - Stephen Marsh ////////// 

ContactSheetII.main();

"Auto-Save-to-TIFF-From-Contact-Sheet-II.jsx";

////////// Original Script Extended 2021 - Stephen Marsh ////////// 
/*

Save All Open Docs to JPG.jsx
Stephen Marsh 2021

Auto-Save File After Creating Contact Sheet
https://community.adobe.com/t5/photoshop/auto-save-file-after-creating-contact-sheet/td-p/11879020

*/

/* Start Open Document Error Check - Part A: If */
if (app.documents.length > 0) {

    /* Main Code Start */

    (function () {

        // Count open files
        var openFileCount = app.documents.length;

        // Loop through all open docs until no docs are open
        while (app.documents.length > 0) {
            saveOpenDocs();
        }

        // End of script notification
        alert(openFileCount + ' files saved to:' + '\r' + theOutputFolder.fsName);

        function saveOpenDocs() {
            var theSuffix = app.activeDocument.name.replace(/(^.+)(-\d+)/, '$2');
            var docName = theOutputFolder.name;
            var saveFileTIFF = new File(new File(theOutputFolder + '/' + docName + theSuffix + '.tif'));
            saveTIFF(saveFileTIFF);
            app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);

            // Setup TIFF options
            function saveTIFF(saveFileTIFF) {
                var tifOpts = new TiffSaveOptions();
                tifOpts.embedColorProfile = true;
                tifOpts.imageCompression = TIFFEncoding.NONE;
                tifOpts.alphaChannels = true;
                tifOpts.byteOrder = ByteOrder.IBM;
                tifOpts.layers = false;
                app.activeDocument.saveAs(saveFileTIFF, tifOpts, true, Extension.LOWERCASE);
            }
        }

        // Open the output folder
        theOutputFolder.execute();

    })();

    /* Main Code Finish */
}
/* Finish Open Document Error Check - Part A: If */

/* Start Open Document Error Check - Part B: Else */
else {
}
/* Finish Open Document Error Check - Part B: Else */
////////// Original Script Extended 2021 - Stephen Marsh ////////// 

// EOF

// must be the last thing, if user hits cancel on the dialog then no action/event gets recorded
gScriptResult;
