//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.desktop.NativeApplication;
import flash.display.NativeMenu;
import flash.display.NativeMenuItem;
import flash.display.NativeWindow;
import flash.display.Stage;
import flash.display.StageQuality;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filesystem.File;
import flash.net.SharedObject;
import flash.utils.IDataOutput;

import flump.executor.Executor;
import flump.executor.Future;
import flump.export.Ternary;
import flump.xfl.ParseError;
import flump.xfl.XflLibrary;

import mx.collections.ArrayList;
import mx.events.CollectionEvent;
import mx.events.PropertyChangeEvent;

import spark.components.DataGrid;
import spark.components.DropDownList;
import spark.components.List;
import spark.components.Window;
import spark.events.GridSelectionEvent;

import com.threerings.util.F;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;

import starling.display.Sprite;

public class Exporter
{
    public static const NA :NativeApplication = NativeApplication.nativeApplication;

    public function Exporter (win :ExporterWindow) {
        Log.setLevel("", Log.INFO);
        _win = win;
        _errors = _win.errors;
        _libraries = _win.libraries;

        function updatePreviewAndExport (..._) :void {
            _win.export.enabled = _exportChooser.dir != null && _libraries.selectionLength > 0 &&
                _libraries.selectedItems.some(function (status :DocStatus, ..._) :Boolean {
                    return status.isValid;
            });

            var status :DocStatus = _libraries.selectedItem as DocStatus;
            _win.preview.enabled = status != null && status.isValid;

            if (_exportChooser.dir == null) return;
            _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
        }

        var fileMenuItem :NativeMenuItem;
        if (NativeApplication.supportsMenu) {
            // Grab the existing menu on macs. Use an index to get it as it's not going to be
            // 'File' in all languages
            fileMenuItem = NA.menu.getItemAt(1);
            // Add a separator before the existing close command
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 0);
        } else {
            _win.nativeWindow.menu = new NativeMenu();
            fileMenuItem = _win.nativeWindow.menu.addSubmenu(new NativeMenu(), "File");
        }

        // Add save and save as by index to work with the existing items on Mac
        // Mac menus have an existing "Close" item, so everything we add should go ahead of that
        var newMenuItem :NativeMenuItem = fileMenuItem.submenu.addItemAt(new NativeMenuItem("New"), 0);
        newMenuItem.keyEquivalent = "n";
        newMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            _confFile = null;
            _conf = new FlumpConf();
            setImport(null);
            updatePublisher();
        });
        var openMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Open..."), 1);
        openMenuItem.keyEquivalent = "o";
        openMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                openConf();
                updatePublisher();
            });
            file.browseForOpen("Open Flump Configuration");
        });
        fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 2);

        const saveMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save"), 3);
        saveMenuItem.keyEquivalent = "s";
        function saveConf () :void {
            Files.write(_confFile, function (out :IDataOutput) :void {
                // Set directories relative to where this file is being saved. Fall back to absolute
                // paths if relative paths aren't possible.
                if (_importChooser.dir != null) {
                    _conf.importDir = _confFile.parent.getRelativePath(_importChooser.dir, /*useDotDot=*/true);
                    if (_conf.importDir == null) _conf.importDir = _importChooser.dir.nativePath;
                }

                if (_exportChooser.dir != null) {
                    _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
                    if (_conf.exportDir == null) _conf.exportDir = _exportChooser.dir.nativePath;
                }

                out.writeUTFBytes(JSON.stringify(_conf, null, /*space=*/2));
            });
        };
        function saveAs (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                trace("Conf file is now " + _confFile.nativePath);
                _settings.data["CONF_FILE"] = _confFile.nativePath;
                _settings.flush();
                saveConf();
            });
            file.browseForSave("Save Flump Configuration");
        };
        saveMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            if (_confFile == null) saveAs();
            else saveConf();
        });

        function openConf () :void {
            try {
                _conf = FlumpConf.fromJSON(JSONFormat.readJSON(_confFile));
                _win.title = _confFile.name;
                var dir :String = _confFile.parent.resolvePath(_conf.importDir).nativePath;
                setImport(new File(dir));
            } catch (e :Error) {
                log.warning("Unable to parse conf", e);
                _errors.dataProvider.addItem(new ParseError(_confFile.nativePath,
                    ParseError.CRIT, "Unable to read configuration"));
                _confFile = null;
            }
        };

        const saveAsMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save As..."), 4);
        saveAsMenuItem.keyEquivalent = "S";
        saveAsMenuItem.addEventListener(Event.SELECT, saveAs);

        if (_settings.data.hasOwnProperty("CONF_FILE")) {
            _confFile = new File(_settings.data["CONF_FILE"]);
            openConf();
        }

        var curSelection :DocStatus = null;
        _libraries.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
            log.info("Changed", "selected", _libraries.selectedIndices);
            updatePreviewAndExport();

            if (curSelection != null) {
                curSelection.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            var newSelection :DocStatus = _libraries.selectedItem as DocStatus;
            if (newSelection != null) {
                newSelection.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            curSelection = newSelection;
        });
        _win.reload.addEventListener(MouseEvent.CLICK, function (..._) :void {
            setImport(_importChooser.dir);
            updatePreviewAndExport();
        });
        _win.export.addEventListener(MouseEvent.CLICK, function (..._) :void {
            for each (var status :DocStatus in _libraries.selectedItems) {
                exportFlashDocument(status);
            }
        });
        _win.preview.addEventListener(MouseEvent.CLICK, function (..._) :void {
            showPreviewWindow(_libraries.selectedItem.lib);
        });
        _importChooser = new DirChooser(null, _win.importRoot, _win.browseImport);
        _importChooser.changed.add(setImport);
        _exportChooser = new DirChooser(null, _win.exportRoot, _win.browseExport);
        _exportChooser.changed.add(updatePreviewAndExport);

        function updatePublisher (..._) :void {
            if (_confFile != null) {
                _importChooser.dir = (_conf.importDir != null) ? _confFile.parent.resolvePath(_conf.importDir) : null;
                _exportChooser.dir = (_conf.exportDir != null) ? _confFile.parent.resolvePath(_conf.exportDir) : null;
            } else {
                _importChooser.dir = null;
                _exportChooser.dir = null;
            }
            if (_exportChooser.dir == null || _conf.exports.length == 0) _publisher = null;
            else _publisher = new Publisher(_exportChooser.dir, Vector.<ExportConf>(_conf.exports));

            var formatNames :Array = [];
            for each (var export :ExportConf in _conf.exports) formatNames.push(export.name);
            _win.formatOverview.text = formatNames.join(", ");
        };

        var editFormats :EditFormatsWindow;
        _win.editFormats.addEventListener(MouseEvent.CLICK, function (..._) :void {
            if (editFormats == null || editFormats.closed) {
                editFormats = new EditFormatsWindow();
                editFormats.open();
            } else editFormats.orderToFront();

            var dataProvider :ArrayList = new ArrayList(_conf.exports);
            dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE, updatePublisher);

            editFormats.exports.dataProvider = dataProvider;
            editFormats.buttonAdd.addEventListener(MouseEvent.CLICK, function (..._) :void {
                var export :ExportConf = new ExportConf();
                export.name = "format" + (_conf.exports.length+1);
                if (_conf.exports.length > 0) {
                    export.format = _conf.exports[0].format;
                }
                dataProvider.addItem(export);
            });
            editFormats.exports.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
                editFormats.buttonRemove.enabled = (editFormats.exports.selectedItem != null);
            });
            editFormats.buttonRemove.addEventListener(MouseEvent.CLICK, function (..._) :void {
                for each (var export :ExportConf in editFormats.exports.selectedItems) {
                    dataProvider.removeItem(export);
                }
            });
        });

        updatePublisher();
        _win.addEventListener(Event.CLOSE, function (..._) :void { NA.exit(0); });
    }

    protected function setImport (root :File) :void {
        _libraries.dataProvider.removeAll();
        _errors.dataProvider.removeAll();
        if (root == null) return;
        _rootLen = root.nativePath.length + 1;
        if (_docFinder != null) _docFinder.shutdownNow();
        _docFinder = new Executor();
        findFlashDocuments(root, _docFinder, true);
        _win.reload.enabled = true;
    }

    protected function showPreviewWindow (lib :XflLibrary) :void {
        if (_previewController == null || _previewWindow.closed || _previewControls.closed) {
            _previewWindow = new PreviewWindow();
            _previewControls = new PreviewControlsWindow();
            _previewWindow.started = function (container :Sprite) :void {
                _previewController = new PreviewController(lib, container, _previewWindow,
                    _previewControls);
            }

            _previewWindow.open();
            _previewControls.open();

            preventWindowClose(_previewWindow.nativeWindow);
            preventWindowClose(_previewControls.nativeWindow);

        } else {
            _previewController.lib = lib;
            _previewWindow.nativeWindow.visible = true;
            _previewControls.nativeWindow.visible = true;
        }

        _previewWindow.orderToFront();
        _previewControls.orderToFront();
    }

    // Causes a window to be hidden, rather than closed, when its close box is clicked
    protected static function preventWindowClose (window :NativeWindow) :void {
        window.addEventListener(Event.CLOSING, function (e :Event) :void {
            e.preventDefault();
            window.visible = false;
        });
    }

    protected var _previewController :PreviewController;
    protected var _previewWindow :PreviewWindow;
    protected var _previewControls :PreviewControlsWindow;

    protected function findFlashDocuments (base :File, exec :Executor,
        ignoreXflAtBase :Boolean = false) :void {
        Files.list(base, exec).succeeded.add(function (files :Array) :void {
            if (exec.isShutdown) return;
            for each (var file :File in files) {
                if (Files.hasExtension(file, "xfl")) {
                    if (ignoreXflAtBase) {
                        _errors.dataProvider.addItem(new ParseError(base.nativePath,
                            ParseError.CRIT, "The import directory can't be an XFL directory, did you mean " +
                            base.parent.nativePath + "?"));
                    } else addFlashDocument(file);
                    return;
                }
            }
            for each (file in files) {
                if (StringUtil.startsWith(file.name, ".", "RECOVER_")) {
                    continue; // Ignore hidden VCS directories, and recovered backups created by Flash
                }
                if (file.isDirectory) findFlashDocuments(file, exec);
                else addFlashDocument(file);
            }
        });
    }

    protected function exportFlashDocument (status :DocStatus) :void {
        const stage :Stage = NA.activeWindow.stage;
        const prevQuality :String = stage.quality;

        stage.quality = StageQuality.BEST;
        _publisher.publish(status.lib);

        stage.quality = prevQuality;
        status.updateModified(Ternary.FALSE);
    }

    protected function addFlashDocument (file :File) :void {
        var name :String = file.nativePath.substring(_rootLen).replace(
            new RegExp("\\" + File.separator, "g"), "/");
        var load :Future;
        switch (Files.getExtension(file)) {
        case "xfl":
            name = name.substr(0, name.lastIndexOf("/"));
            load = new XflLoader().load(name, file.parent);
            break;
        case "fla":
            name = name.substr(0, name.lastIndexOf("."));
            load = new FlaLoader().load(name, file);
            break;
        default:
            // Unsupported file type, ignore
            return;
        }

        const status :DocStatus = new DocStatus(name, _rootLen, Ternary.UNKNOWN, Ternary.UNKNOWN, null);
        _libraries.dataProvider.addItem(status);

        load.succeeded.add(function (lib :XflLibrary) :void {
            status.lib = lib;
            status.updateModified(Ternary.of(_publisher == null || _publisher.modified(lib)));
            for each (var err :ParseError in lib.getErrors()) _errors.dataProvider.addItem(err);
            status.updateValid(Ternary.of(lib.valid));
        });
        load.failed.add(function (error :Error) :void {
            trace("Failed to load " + file.nativePath + ": " + error);
            status.updateValid(Ternary.FALSE);
            throw error;
        });
    }

    protected var _rootLen :int;

    protected var _publisher :Publisher;
    protected var _docFinder :Executor;
    protected var _win :ExporterWindow;
    protected var _libraries :DataGrid;
    protected var _errors :DataGrid;
    protected var _exportChooser :DirChooser;
    protected var _importChooser :DirChooser;
    protected var _authoredResolution :DropDownList;
    protected var _conf :FlumpConf = new FlumpConf();
    protected var _confFile :File;
    protected const _settings :SharedObject = SharedObject.getLocal("flump/Exporter");

    private static const log :Log = Log.getLog(Exporter);
}
}

import flash.events.EventDispatcher;

import flump.export.Ternary;
import flump.xfl.XflLibrary;

import mx.core.IPropertyChangeNotifier;
import mx.events.PropertyChangeEvent;

class DocStatus extends EventDispatcher implements IPropertyChangeNotifier {
    public var path :String;
    public var modified :String;
    public var valid :String = QUESTION;
    public var lib :XflLibrary;

    public function DocStatus (path :String, rootLen :int, modified :Ternary, valid :Ternary, lib :XflLibrary) {
        this.lib = lib;
        this.path = path;
        _uid = path;

        updateModified(modified);
        updateValid(valid);
    }

    public function updateValid (newValid :Ternary) :void {
        changeField("valid", function (..._) :void {
            if (newValid == Ternary.TRUE) valid = CHECK;
            else if (newValid == Ternary.FALSE) valid = FROWN;
            else valid = QUESTION;
        });
    }

    public function get isValid () :Boolean { return valid == CHECK; }

    public function updateModified (newModified :Ternary) :void {
        changeField("modified", function (..._) :void {
            if (newModified == Ternary.TRUE) modified = CHECK;
            else if (newModified == Ternary.FALSE) modified = " ";
            else modified = QUESTION;
        });
    }

    protected function changeField(fieldName :String, modifier :Function) :void {
        const oldValue :Object = this[fieldName];
        modifier();
        const newValue :Object = this[fieldName];
        dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, fieldName, oldValue, newValue));
    }

    public function get uid () :String { return _uid; }
    public function set uid (uid :String) :void { _uid = uid; }

    protected var _uid :String;

    protected static const QUESTION :String = "?";
    protected static const FROWN :String = "☹";
    protected static const CHECK :String = "✓";
}

//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.desktop.NativeApplication;
import flash.display.NativeMenu;
import flash.display.NativeMenuItem;
import flash.display.NativeWindow;
import flash.display.Stage;
import flash.display.StageQuality;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filesystem.File;
import flash.net.SharedObject;
import flash.utils.IDataOutput;

import flump.executor.Executor;
import flump.executor.Future;
import flump.export.Ternary;
import flump.xfl.ParseError;
import flump.xfl.XflLibrary;

import mx.collections.ArrayList;
import mx.events.CollectionEvent;
import mx.events.PropertyChangeEvent;

import spark.components.DataGrid;
import spark.components.DropDownList;
import spark.components.List;
import spark.components.Window;
import spark.events.GridSelectionEvent;

import com.threerings.util.F;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;

import starling.display.Sprite;

public class Exporter
{
    public static const NA :NativeApplication = NativeApplication.nativeApplication;

    public function Exporter (win :ExporterWindow) {
        Log.setLevel("", Log.INFO);
        _win = win;
        _errors = _win.errors;
        _libraries = _win.libraries;

        function updatePreviewAndExport (..._) :void {
            _win.export.enabled = _exportChooser.dir != null && _libraries.selectionLength > 0 &&
                _libraries.selectedItems.some(function (status :DocStatus, ..._) :Boolean {
                    return status.isValid;
            });

            var status :DocStatus = _libraries.selectedItem as DocStatus;
            _win.preview.enabled = status != null && status.isValid;

            if (_exportChooser.dir == null) return;
            _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
        }

        var fileMenuItem :NativeMenuItem;
        if (NativeApplication.supportsMenu) {
            // Grab the existing menu on macs. Use an index to get it as it's not going to be
            // 'File' in all languages
            fileMenuItem = NA.menu.getItemAt(1);
            // Add a separator before the existing close command
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 0);
        } else {
            _win.nativeWindow.menu = new NativeMenu();
            fileMenuItem = _win.nativeWindow.menu.addSubmenu(new NativeMenu(), "File");
        }

        // Add save and save as by index to work with the existing items on Mac
        // Mac menus have an existing "Close" item, so everything we add should go ahead of that
        var newMenuItem :NativeMenuItem = fileMenuItem.submenu.addItemAt(new NativeMenuItem("New"), 0);
        newMenuItem.keyEquivalent = "n";
        newMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            _confFile = null;
            _conf = new FlumpConf();
            setImport(null);
            updatePublisher();
        });
        var openMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Open..."), 1);
        openMenuItem.keyEquivalent = "o";
        openMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                openConf();
                updatePublisher();
                updateWindowTitle(false);
            });
            file.browseForOpen("Open Flump Configuration");
        });
        fileMenuItem.submenu.addItemAt(new NativeMenuItem("Sep", /*separator=*/true), 2);

        const saveMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save"), 3);
        saveMenuItem.keyEquivalent = "s";
        function saveConf () :void {
            Files.write(_confFile, function (out :IDataOutput) :void {
                // Set directories relative to where this file is being saved. Fall back to absolute
                // paths if relative paths aren't possible.
                if (_importChooser.dir != null) {
                    _conf.importDir = _confFile.parent.getRelativePath(_importChooser.dir, /*useDotDot=*/true);
                    if (_conf.importDir == null) _conf.importDir = _importChooser.dir.nativePath;
                }

                if (_exportChooser.dir != null) {
                    _conf.exportDir = _confFile.parent.getRelativePath(_exportChooser.dir, /*useDotDot=*/true);
                    if (_conf.exportDir == null) _conf.exportDir = _exportChooser.dir.nativePath;
                }

                out.writeUTFBytes(JSON.stringify(_conf, null, /*space=*/2));
                updateWindowTitle(false);
            });
        };
        function saveAs (..._) :void {
            var file :File = new File();
            file.addEventListener(Event.SELECT, function (..._) :void {
                _confFile = file;
                trace("Conf file is now " + _confFile.nativePath);
                _settings.data["CONF_FILE"] = _confFile.nativePath;
                _settings.flush();
                saveConf();
            });
            file.browseForSave("Save Flump Configuration");
        };
        saveMenuItem.addEventListener(Event.SELECT, function (..._) :void {
            if (_confFile == null) saveAs();
            else saveConf();
        });

        function updateWindowTitle (modified :Boolean) :void {
            var name :String = (_confFile != null) ? _confFile.name : "Untitled";
            if (modified) name += "*";
            _win.title = name;
        };

        function openConf () :void {
            try {
                _conf = FlumpConf.fromJSON(JSONFormat.readJSON(_confFile));
                var dir :String = _confFile.parent.resolvePath(_conf.importDir).nativePath;
                setImport(new File(dir));
            } catch (e :Error) {
                log.warning("Unable to parse conf", e);
                _errors.dataProvider.addItem(new ParseError(_confFile.nativePath,
                    ParseError.CRIT, "Unable to read configuration"));
                _confFile = null;
            }
        };

        const saveAsMenuItem :NativeMenuItem =
            fileMenuItem.submenu.addItemAt(new NativeMenuItem("Save As..."), 4);
        saveAsMenuItem.keyEquivalent = "S";
        saveAsMenuItem.addEventListener(Event.SELECT, saveAs);

        if (_settings.data.hasOwnProperty("CONF_FILE")) {
            _confFile = new File(_settings.data["CONF_FILE"]);
            openConf();
        }

        var curSelection :DocStatus = null;
        _libraries.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
            log.info("Changed", "selected", _libraries.selectedIndices);
            updatePreviewAndExport();

            if (curSelection != null) {
                curSelection.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            var newSelection :DocStatus = _libraries.selectedItem as DocStatus;
            if (newSelection != null) {
                newSelection.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, updatePreviewAndExport);
            }
            curSelection = newSelection;
        });
        _win.reload.addEventListener(MouseEvent.CLICK, function (..._) :void {
            setImport(_importChooser.dir);
            updatePreviewAndExport();
        });
        _win.export.addEventListener(MouseEvent.CLICK, function (..._) :void {
            for each (var status :DocStatus in _libraries.selectedItems) {
                exportFlashDocument(status);
            }
        });
        _win.preview.addEventListener(MouseEvent.CLICK, function (..._) :void {
            showPreviewWindow(_libraries.selectedItem.lib);
        });
        _importChooser = new DirChooser(null, _win.importRoot, _win.browseImport);
        _importChooser.changed.add(setImport);
        _exportChooser = new DirChooser(null, _win.exportRoot, _win.browseExport);
        _exportChooser.changed.add(updatePreviewAndExport);

        _importChooser.changed.add(F.callback(updateWindowTitle, true));
        _exportChooser.changed.add(F.callback(updateWindowTitle, true));

        function updatePublisher (..._) :void {
            if (_confFile != null) {
                _importChooser.dir = (_conf.importDir != null) ? _confFile.parent.resolvePath(_conf.importDir) : null;
                _exportChooser.dir = (_conf.exportDir != null) ? _confFile.parent.resolvePath(_conf.exportDir) : null;
            } else {
                _importChooser.dir = null;
                _exportChooser.dir = null;
            }
            if (_exportChooser.dir == null || _conf.exports.length == 0) _publisher = null;
            else _publisher = new Publisher(_exportChooser.dir, Vector.<ExportConf>(_conf.exports));

            var formatNames :Array = [];
            for each (var export :ExportConf in _conf.exports) formatNames.push(export.name);
            _win.formatOverview.text = formatNames.join(", ");

            updateWindowTitle(true);
        };

        var editFormats :EditFormatsWindow;
        _win.editFormats.addEventListener(MouseEvent.CLICK, function (..._) :void {
            if (editFormats == null || editFormats.closed) {
                editFormats = new EditFormatsWindow();
                editFormats.open();
            } else editFormats.orderToFront();

            var dataProvider :ArrayList = new ArrayList(_conf.exports);
            dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE, updatePublisher);

            editFormats.exports.dataProvider = dataProvider;
            editFormats.buttonAdd.addEventListener(MouseEvent.CLICK, function (..._) :void {
                var export :ExportConf = new ExportConf();
                export.name = "format" + (_conf.exports.length+1);
                if (_conf.exports.length > 0) {
                    export.format = _conf.exports[0].format;
                }
                dataProvider.addItem(export);
            });
            editFormats.exports.addEventListener(GridSelectionEvent.SELECTION_CHANGE, function (..._) :void {
                editFormats.buttonRemove.enabled = (editFormats.exports.selectedItem != null);
            });
            editFormats.buttonRemove.addEventListener(MouseEvent.CLICK, function (..._) :void {
                for each (var export :ExportConf in editFormats.exports.selectedItems) {
                    dataProvider.removeItem(export);
                }
            });
        });

        updatePublisher();
        updateWindowTitle(false);
        _win.addEventListener(Event.CLOSE, function (..._) :void { NA.exit(0); });
    }

    protected function setImport (root :File) :void {
        _libraries.dataProvider.removeAll();
        _errors.dataProvider.removeAll();
        if (root == null) return;
        _rootLen = root.nativePath.length + 1;
        if (_docFinder != null) _docFinder.shutdownNow();
        _docFinder = new Executor();
        findFlashDocuments(root, _docFinder, true);
        _win.reload.enabled = true;
    }

    protected function showPreviewWindow (lib :XflLibrary) :void {
        if (_previewController == null || _previewWindow.closed || _previewControls.closed) {
            _previewWindow = new PreviewWindow();
            _previewControls = new PreviewControlsWindow();
            _previewWindow.started = function (container :Sprite) :void {
                _previewController = new PreviewController(lib, container, _previewWindow,
                    _previewControls);
            }

            _previewWindow.open();
            _previewControls.open();

            preventWindowClose(_previewWindow.nativeWindow);
            preventWindowClose(_previewControls.nativeWindow);

        } else {
            _previewController.lib = lib;
            _previewWindow.nativeWindow.visible = true;
            _previewControls.nativeWindow.visible = true;
        }

        _previewWindow.orderToFront();
        _previewControls.orderToFront();
    }

    // Causes a window to be hidden, rather than closed, when its close box is clicked
    protected static function preventWindowClose (window :NativeWindow) :void {
        window.addEventListener(Event.CLOSING, function (e :Event) :void {
            e.preventDefault();
            window.visible = false;
        });
    }

    protected var _previewController :PreviewController;
    protected var _previewWindow :PreviewWindow;
    protected var _previewControls :PreviewControlsWindow;

    protected function findFlashDocuments (base :File, exec :Executor,
        ignoreXflAtBase :Boolean = false) :void {
        Files.list(base, exec).succeeded.add(function (files :Array) :void {
            if (exec.isShutdown) return;
            for each (var file :File in files) {
                if (Files.hasExtension(file, "xfl")) {
                    if (ignoreXflAtBase) {
                        _errors.dataProvider.addItem(new ParseError(base.nativePath,
                            ParseError.CRIT, "The import directory can't be an XFL directory, did you mean " +
                            base.parent.nativePath + "?"));
                    } else addFlashDocument(file);
                    return;
                }
            }
            for each (file in files) {
                if (StringUtil.startsWith(file.name, ".", "RECOVER_")) {
                    continue; // Ignore hidden VCS directories, and recovered backups created by Flash
                }
                if (file.isDirectory) findFlashDocuments(file, exec);
                else addFlashDocument(file);
            }
        });
    }

    protected function exportFlashDocument (status :DocStatus) :void {
        const stage :Stage = NA.activeWindow.stage;
        const prevQuality :String = stage.quality;

        stage.quality = StageQuality.BEST;
        _publisher.publish(status.lib);

        stage.quality = prevQuality;
        status.updateModified(Ternary.FALSE);
    }

    protected function addFlashDocument (file :File) :void {
        var name :String = file.nativePath.substring(_rootLen).replace(
            new RegExp("\\" + File.separator, "g"), "/");
        var load :Future;
        switch (Files.getExtension(file)) {
        case "xfl":
            name = name.substr(0, name.lastIndexOf("/"));
            load = new XflLoader().load(name, file.parent);
            break;
        case "fla":
            name = name.substr(0, name.lastIndexOf("."));
            load = new FlaLoader().load(name, file);
            break;
        default:
            // Unsupported file type, ignore
            return;
        }

        const status :DocStatus = new DocStatus(name, _rootLen, Ternary.UNKNOWN, Ternary.UNKNOWN, null);
        _libraries.dataProvider.addItem(status);

        load.succeeded.add(function (lib :XflLibrary) :void {
            status.lib = lib;
            status.updateModified(Ternary.of(_publisher == null || _publisher.modified(lib)));
            for each (var err :ParseError in lib.getErrors()) _errors.dataProvider.addItem(err);
            status.updateValid(Ternary.of(lib.valid));
        });
        load.failed.add(function (error :Error) :void {
            trace("Failed to load " + file.nativePath + ": " + error);
            status.updateValid(Ternary.FALSE);
            throw error;
        });
    }

    protected var _rootLen :int;

    protected var _publisher :Publisher;
    protected var _docFinder :Executor;
    protected var _win :ExporterWindow;
    protected var _libraries :DataGrid;
    protected var _errors :DataGrid;
    protected var _exportChooser :DirChooser;
    protected var _importChooser :DirChooser;
    protected var _authoredResolution :DropDownList;
    protected var _conf :FlumpConf = new FlumpConf();
    protected var _confFile :File;
    protected const _settings :SharedObject = SharedObject.getLocal("flump/Exporter");

    private static const log :Log = Log.getLog(Exporter);
}
}

import flash.events.EventDispatcher;

import flump.export.Ternary;
import flump.xfl.XflLibrary;

import mx.core.IPropertyChangeNotifier;
import mx.events.PropertyChangeEvent;

class DocStatus extends EventDispatcher implements IPropertyChangeNotifier {
    public var path :String;
    public var modified :String;
    public var valid :String = QUESTION;
    public var lib :XflLibrary;

    public function DocStatus (path :String, rootLen :int, modified :Ternary, valid :Ternary, lib :XflLibrary) {
        this.lib = lib;
        this.path = path;
        _uid = path;

        updateModified(modified);
        updateValid(valid);
    }

    public function updateValid (newValid :Ternary) :void {
        changeField("valid", function (..._) :void {
            if (newValid == Ternary.TRUE) valid = CHECK;
            else if (newValid == Ternary.FALSE) valid = FROWN;
            else valid = QUESTION;
        });
    }

    public function get isValid () :Boolean { return valid == CHECK; }

    public function updateModified (newModified :Ternary) :void {
        changeField("modified", function (..._) :void {
            if (newModified == Ternary.TRUE) modified = CHECK;
            else if (newModified == Ternary.FALSE) modified = " ";
            else modified = QUESTION;
        });
    }

    protected function changeField(fieldName :String, modifier :Function) :void {
        const oldValue :Object = this[fieldName];
        modifier();
        const newValue :Object = this[fieldName];
        dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, fieldName, oldValue, newValue));
    }

    public function get uid () :String { return _uid; }
    public function set uid (uid :String) :void { _uid = uid; }

    protected var _uid :String;

    protected static const QUESTION :String = "?";
    protected static const FROWN :String = "☹";
    protected static const CHECK :String = "✓";
}

Put an asterisk in the window title when you have unsaved changes.


package com.kaltura.kdpfl
{
	import com.kaltura.kdpfl.controller.InitMacroCommand;
	import com.kaltura.kdpfl.controller.LayoutReadyCommand;
	import com.kaltura.kdpfl.controller.PlaybackCompleteCommand;
	import com.kaltura.kdpfl.controller.SequenceItemPlayEndCommand;
	import com.kaltura.kdpfl.controller.SequenceSkipNextCommand;
	import com.kaltura.kdpfl.controller.StartupCommand;
	import com.kaltura.kdpfl.controller.media.InitChangeMediaMacroCommand;
	import com.kaltura.kdpfl.controller.media.LiveStreamCommand;
	import com.kaltura.kdpfl.controller.media.MediaReadyCommand;
	import com.kaltura.kdpfl.events.DynamicEvent;
	import com.kaltura.kdpfl.model.ExternalInterfaceProxy;
	import com.kaltura.kdpfl.model.type.DebugLevel;
	import com.kaltura.kdpfl.model.type.NotificationType;
	import com.kaltura.kdpfl.view.controls.KTrace;
	import com.kaltura.kdpfl.view.media.KMediaPlayerMediator;
	import com.kaltura.puremvc.as3.core.KView;

	import flash.display.DisplayObject;

	import mx.utils.ObjectProxy;

	import org.osmf.media.MediaPlayerState;
	import org.puremvc.as3.interfaces.IFacade;
	import org.puremvc.as3.interfaces.IMediator;
	import org.puremvc.as3.interfaces.IProxy;
	import org.puremvc.as3.patterns.facade.Facade;

	public class ApplicationFacade extends Facade implements IFacade
	{
		/**
		 * The current version of the KDP.
		 */
		public var kdpVersion : String = "v3.8.9.rc1";

		/**
		 * save any mediator name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _mediatorNameArray : Array = new Array();
		/**
		 * save any proxy name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _proxyNameArray : Array = new Array();
		/**
		 * save any notification that create a command name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _commandNameArray : Array = new Array();

		/**
		 * The bindObject create dynamicly all attributes that need to be binded on it
		 */
		public var bindObject:ObjectProxy = new ObjectProxy();
		/**
		 * a reference to KDP3 main application
		 */
		private var _app : DisplayObject;

		/**
		 * the url of the kdp
		 */
		public var appFolder : String;
		public var debugMode : Boolean = false;
		/**
		 *
		 * will affect the traces that will be sent. See DebugLevel enum
		 */
		public var debugLevel:int;

		private var externalInterface : ExternalInterfaceProxy;

		/**
		 * return the one and only instance of the ApplicationFacade,
		 * if not exist it will be created.
		 * @return
		 *
		 */
		public static function getInstance() : ApplicationFacade
		{
			if (instance == null) instance = new ApplicationFacade();
			return instance as ApplicationFacade;
		}

		/**
		 * All this simply does is fire a notification which is routed to
		 * "StartupCommand" via the "registerCommand" handlers
		 * @param app
		 *
		 */
		public function start(app:DisplayObject):void
		{
			CONFIG::isSDK46 {
				kdpVersion += ".sdk46";
			}

			_app = app;
			appFolder = app.root.loaderInfo.url;
			appFolder = appFolder.substr(0, appFolder.lastIndexOf('/') + 1);

			sendNotification(NotificationType.STARTUP, app);
			externalInterface = retrieveProxy(ExternalInterfaceProxy.NAME) as ExternalInterfaceProxy;
		}

		/**
		 * Created to trace all notifications for debug
		 * @param notificationName
		 * @param body
		 * @param type
		 *
		 */
		override public function sendNotification(notificationName:String, body:Object = null, type:String = null):void {
			if (debugMode) {
				var s:String;
				if ((notificationName == NotificationType.BYTES_DOWNLOADED_CHANGE && debugLevel == DebugLevel.HIGH) ||
					(notificationName == NotificationType.PLAYER_UPDATE_PLAYHEAD && (debugLevel == DebugLevel.HIGH || debugLevel == DebugLevel.MEDIUM)) ||
					(notificationName != NotificationType.PLAYER_UPDATE_PLAYHEAD && notificationName != NotificationType.BYTES_DOWNLOADED_CHANGE))
				 {
					if (notificationName == NotificationType.PLAYER_STATE_CHANGE)
						s = 'Sent ' + notificationName + ' ==> ' + body.toString();
					else {
						s = 'Sent ' + notificationName;
						if (body) {
							var found:Boolean = false;
							for (var o:* in body) {
								s += ", " + o + ":" + body[o];
								found = true;
							}
							if (!found) {
								s += ", " + body.toString();
							}
						}
					}
				}

				if (s && s != "null") {
					var curTime:Number = 0;
					var kmediaPlayerMediator:KMediaPlayerMediator = retrieveMediator(KMediaPlayerMediator.NAME) as KMediaPlayerMediator;
					if (kmediaPlayerMediator &&
						kmediaPlayerMediator.player &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.LOADING &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.UNINITIALIZED &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.PLAYBACK_ERROR)
					{
						curTime = kmediaPlayerMediator.getCurrentTime();
					}
					var date:Date = new Date();

					KTrace.getInstance().log(date.toTimeString(), ":", s, ", playhead position:", curTime);
				}

			}

			super.sendNotification(notificationName, body, type);
			//For external Flash/Flex application application listening to the KDP events
			_app.dispatchEvent(new DynamicEvent(notificationName, body));

			//For external Javascript application listening to the KDP events
			if (externalInterface)
				externalInterface.notifyJs(notificationName, body);

		}

		/**
		 * controls the routing of notifications to our controllers,
		 * we all know them as commands
		 *
		 */
		override protected function initializeController():void
		{
			super.initializeController();
			registerCommand(NotificationType.STARTUP, StartupCommand);

			// we do both init start KDP life cycle, and start to load the entry simultaneously
			registerCommand(NotificationType.INITIATE_APP, InitMacroCommand);

			//There are several things that need to be done as soon as the layout of the kdp is ready.
			registerCommand(NotificationType.LAYOUT_READY, LayoutReadyCommand );
			//when one call change media we fire the load media command again
			registerCommand(NotificationType.CHANGE_MEDIA, InitChangeMediaMacroCommand);

			registerCommand (NotificationType.LIVE_ENTRY, LiveStreamCommand );

			registerCommand (NotificationType.MEDIA_LOADED, MediaReadyCommand );

			registerCommand (NotificationType.PLAYBACK_COMPLETE, PlaybackCompleteCommand);

			registerCommand(NotificationType.SEQUENCE_ITEM_PLAY_END, SequenceItemPlayEndCommand);

			registerCommand(NotificationType.SEQUENCE_SKIP_NEXT, SequenceSkipNextCommand);
		}

		override protected function initializeView():void
		{
			if ( view != null ) return;
			view = KView.getInstance();
		}

		override protected function initializeFacade():void
		{
			initializeView();
			initializeModel();
			initializeController();
		}

		/**
		 * Help to remove all commands, mediator, proxies from the facade
		 *
		 */
		public function dispose() : void
		{
			var i:int =0;

			for(i=0; i<_commandNameArray.length; i++)
				this.removeCommand( _commandNameArray[i] );

			for(i=0; i<_mediatorNameArray.length; i++)
				this.removeMediator( _mediatorNameArray[i] );

			for(i=0; i<_proxyNameArray.length; i++)
				this.removeProxy( _proxyNameArray[i] );

			_commandNameArray = new Array();
			_mediatorNameArray = new Array();
			_proxyNameArray = new Array();
		}

		/**
		 * after registartion add the notification name to a
		 * @param notificationName
		 * @param commandClassRef
		 *
		 */
		override public function registerCommand(notificationName:String, commandClassRef:Class):void
		{
			super.registerCommand(notificationName, commandClassRef);

			//save the notification name so we can delete it later
			_commandNameArray.push( notificationName );
		}

		override public function registerMediator(mediator:IMediator):void
		{
			super.registerMediator(mediator);

			//save the mediator name so we can delete it later
			_mediatorNameArray.push( mediator.getMediatorName() );
		}

		override public function registerProxy(proxy:IProxy):void
		{
			bindObject[proxy.getProxyName()] = proxy.getData();

			super.registerProxy(proxy);

			//save the proxy name so we can delete it later
			_proxyNameArray.push( proxy.getProxyName() );
		}

		/**
		 * add notification observer for a given mediator
		 * @param notification the notification to listen for
		 * @param notificationHandler handler to be called
		 * @param mediator
		 *
		 */
		public function addNotificationInterest(notification:String, notificationHandler:Function, mediator:IMediator):void {
			(view as KView).addNotificationInterest(notification, notificationHandler, mediator);
		}

		public function get app () : DisplayObject
		{
			return _app;
		}

		public function set app (disObj : DisplayObject) : void
		{
			_app = disObj;
		}

	}
}

package com.kaltura.kdpfl
{
	import com.kaltura.kdpfl.controller.InitMacroCommand;
	import com.kaltura.kdpfl.controller.LayoutReadyCommand;
	import com.kaltura.kdpfl.controller.PlaybackCompleteCommand;
	import com.kaltura.kdpfl.controller.SequenceItemPlayEndCommand;
	import com.kaltura.kdpfl.controller.SequenceSkipNextCommand;
	import com.kaltura.kdpfl.controller.StartupCommand;
	import com.kaltura.kdpfl.controller.media.InitChangeMediaMacroCommand;
	import com.kaltura.kdpfl.controller.media.LiveStreamCommand;
	import com.kaltura.kdpfl.controller.media.MediaReadyCommand;
	import com.kaltura.kdpfl.events.DynamicEvent;
	import com.kaltura.kdpfl.model.ExternalInterfaceProxy;
	import com.kaltura.kdpfl.model.type.DebugLevel;
	import com.kaltura.kdpfl.model.type.NotificationType;
	import com.kaltura.kdpfl.view.controls.KTrace;
	import com.kaltura.kdpfl.view.media.KMediaPlayerMediator;
	import com.kaltura.puremvc.as3.core.KView;

	import flash.display.DisplayObject;

	import mx.utils.ObjectProxy;

	import org.osmf.media.MediaPlayerState;
	import org.puremvc.as3.interfaces.IFacade;
	import org.puremvc.as3.interfaces.IMediator;
	import org.puremvc.as3.interfaces.IProxy;
	import org.puremvc.as3.patterns.facade.Facade;

	public class ApplicationFacade extends Facade implements IFacade
	{
		/**
		 * The current version of the KDP.
		 */
		public var kdpVersion : String = "v3.8.9";

		/**
		 * save any mediator name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _mediatorNameArray : Array = new Array();
		/**
		 * save any proxy name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _proxyNameArray : Array = new Array();
		/**
		 * save any notification that create a command name that is registered to this array in order to delete at any time
		 * (the pure mvc don't support it as built-in function)
		 */
		private var _commandNameArray : Array = new Array();

		/**
		 * The bindObject create dynamicly all attributes that need to be binded on it
		 */
		public var bindObject:ObjectProxy = new ObjectProxy();
		/**
		 * a reference to KDP3 main application
		 */
		private var _app : DisplayObject;

		/**
		 * the url of the kdp
		 */
		public var appFolder : String;
		public var debugMode : Boolean = false;
		/**
		 *
		 * will affect the traces that will be sent. See DebugLevel enum
		 */
		public var debugLevel:int;

		private var externalInterface : ExternalInterfaceProxy;

		/**
		 * return the one and only instance of the ApplicationFacade,
		 * if not exist it will be created.
		 * @return
		 *
		 */
		public static function getInstance() : ApplicationFacade
		{
			if (instance == null) instance = new ApplicationFacade();
			return instance as ApplicationFacade;
		}

		/**
		 * All this simply does is fire a notification which is routed to
		 * "StartupCommand" via the "registerCommand" handlers
		 * @param app
		 *
		 */
		public function start(app:DisplayObject):void
		{
			CONFIG::isSDK46 {
				kdpVersion += ".sdk46";
			}

			_app = app;
			appFolder = app.root.loaderInfo.url;
			appFolder = appFolder.substr(0, appFolder.lastIndexOf('/') + 1);

			sendNotification(NotificationType.STARTUP, app);
			externalInterface = retrieveProxy(ExternalInterfaceProxy.NAME) as ExternalInterfaceProxy;
		}

		/**
		 * Created to trace all notifications for debug
		 * @param notificationName
		 * @param body
		 * @param type
		 *
		 */
		override public function sendNotification(notificationName:String, body:Object = null, type:String = null):void {
			if (debugMode) {
				var s:String;
				if ((notificationName == NotificationType.BYTES_DOWNLOADED_CHANGE && debugLevel == DebugLevel.HIGH) ||
					(notificationName == NotificationType.PLAYER_UPDATE_PLAYHEAD && (debugLevel == DebugLevel.HIGH || debugLevel == DebugLevel.MEDIUM)) ||
					(notificationName != NotificationType.PLAYER_UPDATE_PLAYHEAD && notificationName != NotificationType.BYTES_DOWNLOADED_CHANGE))
				 {
					if (notificationName == NotificationType.PLAYER_STATE_CHANGE)
						s = 'Sent ' + notificationName + ' ==> ' + body.toString();
					else {
						s = 'Sent ' + notificationName;
						if (body) {
							var found:Boolean = false;
							for (var o:* in body) {
								s += ", " + o + ":" + body[o];
								found = true;
							}
							if (!found) {
								s += ", " + body.toString();
							}
						}
					}
				}

				if (s && s != "null") {
					var curTime:Number = 0;
					var kmediaPlayerMediator:KMediaPlayerMediator = retrieveMediator(KMediaPlayerMediator.NAME) as KMediaPlayerMediator;
					if (kmediaPlayerMediator &&
						kmediaPlayerMediator.player &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.LOADING &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.UNINITIALIZED &&
						kmediaPlayerMediator.player.state!=MediaPlayerState.PLAYBACK_ERROR)
					{
						curTime = kmediaPlayerMediator.getCurrentTime();
					}
					var date:Date = new Date();

					KTrace.getInstance().log(date.toTimeString(), ":", s, ", playhead position:", curTime);
				}

			}

			super.sendNotification(notificationName, body, type);
			//For external Flash/Flex application application listening to the KDP events
			_app.dispatchEvent(new DynamicEvent(notificationName, body));

			//For external Javascript application listening to the KDP events
			if (externalInterface)
				externalInterface.notifyJs(notificationName, body);

		}

		/**
		 * controls the routing of notifications to our controllers,
		 * we all know them as commands
		 *
		 */
		override protected function initializeController():void
		{
			super.initializeController();
			registerCommand(NotificationType.STARTUP, StartupCommand);

			// we do both init start KDP life cycle, and start to load the entry simultaneously
			registerCommand(NotificationType.INITIATE_APP, InitMacroCommand);

			//There are several things that need to be done as soon as the layout of the kdp is ready.
			registerCommand(NotificationType.LAYOUT_READY, LayoutReadyCommand );
			//when one call change media we fire the load media command again
			registerCommand(NotificationType.CHANGE_MEDIA, InitChangeMediaMacroCommand);

			registerCommand (NotificationType.LIVE_ENTRY, LiveStreamCommand );

			registerCommand (NotificationType.MEDIA_LOADED, MediaReadyCommand );

			registerCommand (NotificationType.PLAYBACK_COMPLETE, PlaybackCompleteCommand);

			registerCommand(NotificationType.SEQUENCE_ITEM_PLAY_END, SequenceItemPlayEndCommand);

			registerCommand(NotificationType.SEQUENCE_SKIP_NEXT, SequenceSkipNextCommand);
		}

		override protected function initializeView():void
		{
			if ( view != null ) return;
			view = KView.getInstance();
		}

		override protected function initializeFacade():void
		{
			initializeView();
			initializeModel();
			initializeController();
		}

		/**
		 * Help to remove all commands, mediator, proxies from the facade
		 *
		 */
		public function dispose() : void
		{
			var i:int =0;

			for(i=0; i<_commandNameArray.length; i++)
				this.removeCommand( _commandNameArray[i] );

			for(i=0; i<_mediatorNameArray.length; i++)
				this.removeMediator( _mediatorNameArray[i] );

			for(i=0; i<_proxyNameArray.length; i++)
				this.removeProxy( _proxyNameArray[i] );

			_commandNameArray = new Array();
			_mediatorNameArray = new Array();
			_proxyNameArray = new Array();
		}

		/**
		 * after registartion add the notification name to a
		 * @param notificationName
		 * @param commandClassRef
		 *
		 */
		override public function registerCommand(notificationName:String, commandClassRef:Class):void
		{
			super.registerCommand(notificationName, commandClassRef);

			//save the notification name so we can delete it later
			_commandNameArray.push( notificationName );
		}

		override public function registerMediator(mediator:IMediator):void
		{
			super.registerMediator(mediator);

			//save the mediator name so we can delete it later
			_mediatorNameArray.push( mediator.getMediatorName() );
		}

		override public function registerProxy(proxy:IProxy):void
		{
			bindObject[proxy.getProxyName()] = proxy.getData();

			super.registerProxy(proxy);

			//save the proxy name so we can delete it later
			_proxyNameArray.push( proxy.getProxyName() );
		}

		/**
		 * add notification observer for a given mediator
		 * @param notification the notification to listen for
		 * @param notificationHandler handler to be called
		 * @param mediator
		 *
		 */
		public function addNotificationInterest(notification:String, notificationHandler:Function, mediator:IMediator):void {
			(view as KView).addNotificationInterest(notification, notificationHandler, mediator);
		}

		public function get app () : DisplayObject
		{
			return _app;
		}

		public function set app (disObj : DisplayObject) : void
		{
			_app = disObj;
		}

	}
}

update version 3.8.9

update version 3.8.9


package
{
        /**
        *   Creates a string with variable substitutions. Very similiar to printf, specially python's printf
        *   @param raw The string to be substituted.
        *   @param rest  The objects to be substitued, can be positional or by properties inside the object (in wich case only one object can be passed)
        *   @return The formated and substitued string.
        *   @example
        *

        *
        *   // objects are substitued in the other they appear
        *
        *   printf("This is an %s lybrary for creating %s", "Actioscript 3.0", "strings");
        *   // outputs: "This is an Actioscript 3.0 lybrary for creating strings";
        *   // you can also format numbers:
        *
        *   printf("You can also display numbers like PI: %f, and format them to a fixed precision, such as PI with 3 decimal places %.3f", Math.PI, Math.PI);
        *   // outputs: " You can also display numbers like PI: 3.141592653589793, and format them to a fixed precision, such as PI with 3 decimal places 3.142"
        *   // Instead of positional (the order of arguments to print f, you can also use propertie of an object):
        *   var userInfo : Object = {
        	"name": "Arthur Debert",
        	"email": "arthur@stimuli.com.br",
        	"website":"http://www.stimuli.com.br/",
        	"ocupation": "developer"
        }
        *
        *   printf("My name is %(name)s and I am a %(ocupation)s. You can read more on my personal %(website)s, or reach me through my %(email)s", userInfo);
        *   // outputs: "My name is Arthur Debert and I am a developer. You can read more on my personal http://www.stimuli.com.br/, or reach me through my arthur@stimuli.com.br"
        *   // you can also use date parts:
        *   var date : Date = new Date();
        *   printf("Today is %d/%m/%Y", date, date, date)
        *
        *

        *   @see br.com.stimuli.string
        */
         public function printf(raw : String, ...rest) : String{
         	/**
			* Pretty ugly!
			*   basicaly
			*   % -> the start of a substitution hole
			*   (some_var_name) -> [optional] used in named substitutions
			*   .xx -> [optional] the precision with witch numbers will be formated
			*   x -> the formatter (string, hexa, float, date part)
			*/
            var SUBS_RE : RegExp = /%(?!^%)(\((?P<var_name>[\w]+[\w_\d]+)\))?(?P<padding>[0-9]{1,2})?(\.(?P<precision>[0-9]+))?(?P<formater>[sxofaAbBcdHIjmMpSUwWxXyYZ])/ig;

			//Return empty string if raw is null, we don't want errors here
			if( raw == null ){
				return "";
			}
			//trace("\n\n" + 'input:"'+ raw+  '" , args:', rest.join(", ")) ;
            var matches : Array = [];
            var result : Object = SUBS_RE.exec(raw);
            var match : Match;
            var runs : int = 0;
            var numMatches : int = 0;
            var numberVariables : int = rest.length;
            // quick check if we find string subs amongst the text to match (something like %(foo)s
            var isPositionalSubts : Boolean = !Boolean(raw.match(/%\(\s*[\w\d_]+\s*\)/));
            var replacementValue : *;
            var formater : String;
            var varName : String;
            var precision : String;
            var padding : String;
            var paddingNum : int;
            var paddingChar:String;

            // matched through the string, creating Match objects for easier later reuse
            while (Boolean(result)){
                match = new Match();
                match.startIndex = result.index;
                match.length = String(result[0]).length;
                match.endIndex = match.startIndex + match.length;
                match.content = String(result[0]);
                // try to get substitution properties
                formater = result["formater"];
                varName = result["var_name"];
                precision = result["precision"];
                padding = result["padding"];
				paddingNum = 0;
				paddingChar = null;
                //trace('formater:', formater, ', varName:', varName, ', precision:', precision, 'padding:', padding);
                if (padding){
                    if (padding.length == 1){
                        paddingNum = int(padding);
                        paddingChar = " ";
                    }else{
                        paddingNum = int (padding.substr(-1, 1));
                        paddingChar = padding.substr(-2, 1);
                        if (paddingChar != "0"){
                            paddingNum *= int(paddingChar);
                            paddingChar = " ";
                        }
                    }
                }
                if (isPositionalSubts){
                    // by position, grab next subs:
                     replacementValue = rest[matches.length];

                }else{
                    // be hash / properties
                    replacementValue = rest[0] == null ? undefined : rest[0][varName];
                }
                // check for bad variable names
                if (replacementValue == undefined){
                   replacementValue = "";
                }
                if (replacementValue != undefined){

	                // format the string accodingly to the formatter
	                if (formater == STRING_FORMATTER){
	                    match.replacement = padString(String(replacementValue), paddingNum, paddingChar);
	                }else if (formater == FLOAT_FORMATER){
	                    // floats, check if we need to truncate precision
	                    if (precision){
	                        match.replacement = padString(
	                                        Number(replacementValue).toFixed( int(precision)),
	                                        paddingNum,
	                                        paddingChar);
	                    }else{
	                        match.replacement = padString(String(replacementValue), paddingNum, paddingChar);
	                    }
	                }else if (formater == INTEGER_FORMATER){
	                    match.replacement = padString(int(replacementValue).toString(), paddingNum, paddingChar);
	                }else if (formater == OCTAL_FORMATER){
	                    match.replacement = "0" + int(replacementValue).toString(8);
	                }else if (formater == HEXA_FORMATER){
	                    match.replacement = "0x" + int(replacementValue).toString(16);
	                }else if(DATES_FORMATERS.indexOf(formater) > -1){
	                    switch (formater){
	                        case DATE_DAY_FORMATTER:
	                            match.replacement = replacementValue["date"];
	                            break;
	                        case DATE_FULLYEAR_FORMATTER:
	                            match.replacement = replacementValue["fullYear"];
	                            break;
	                        case DATE_YEAR_FORMATTER:
	                            match.replacement = String(replacementValue["fullYear"]).substr(2, 2);
	                            break;
	                        case DATE_MONTH_FORMATTER:
	                            match.replacement = String(replacementValue["month"] + 1);
	                            break;
	                        case DATE_HOUR24_FORMATTER:
	                            match.replacement = replacementValue["hours"];
	                            break;
	                        case DATE_HOUR_FORMATTER:
	                            var hours24 : Number = replacementValue["hours"];
	                            match.replacement =  (hours24 -12).toString();
	                            break;
	                        case DATE_HOUR_AMPM_FORMATTER:
	                            match.replacement =  (replacementValue["hours"]  >= 12 ? "p.m" : "a.m");
	                            break;
	                        case DATE_TOLOCALE_FORMATTER:
	                            match.replacement = Date(replacementValue).toLocaleString();
	                            break;
	                        case DATE_MINUTES_FORMATTER:
	                            match.replacement = replacementValue["minutes"];
	                            break;
	                        case DATE_SECONDS_FORMATTER:
	                            match.replacement = replacementValue["seconds"];
	                            break;
	                    }
	                }else{
	                   //no good replacement
	                }
               		matches.push(match);
                }

                // just a small check in case we get stuck: kludge!
                runs ++;
                if (runs > 10000){
                   //something is wrong, breaking out
                    break;
                }
                numMatches ++;
                // iterates next match
                result = SUBS_RE.exec(raw);
            }
            // in case there's nothing to substitute, just return the initial string
            if(matches.length == 0){
                //no matches, returning raw
               return raw;
            }
            // now actually do the substitution, keeping a buffer to be joined at
            //the end for better performance
            var buffer : Array = [];
            var lastMatch : Match;
            // beggininf os string, if it doesn't start with a substitition
            var previous : String = raw.substr(0, matches[0].startIndex);
            var subs : String;
            for each(match in matches){
                // finds out the previous string part and the next substitition
                if (lastMatch){
                    previous = raw.substring(lastMatch.endIndex  ,  match.startIndex);
                }
                buffer.push(previous);
                buffer.push(match.replacement);
                lastMatch = match;

            }
            // buffer the tail of the string: text after the last substitution
            buffer.push(raw.substr(match.endIndex, raw.length - match.endIndex));
            //trace('returning: "'+ buffer.join("")+'"');
            return buffer.join("");
        }
    }

// internal usage
/** @private */
const BAD_VARIABLE_NUMBER : String = "The number of variables to be replaced and template holes don't match";
/** Converts to a string*/
const STRING_FORMATTER : String = "s";
/** Outputs as a Number, can use the precision specifier: %.2sf will output a float with 2 decimal digits.*/
const FLOAT_FORMATER : String = "f";
/** Outputs as an Integer.*/
const INTEGER_FORMATER : String = "d";
/** Converts to an OCTAL number */
const OCTAL_FORMATER : String = "o";
/** Converts to a Hexa number (includes 0x) */
const HEXA_FORMATER : String = "x";
/** @private */
const DATES_FORMATERS : String = "aAbBcDHIjmMpSUwWxXyYZ";
/** Day of month, from 0 to 30 on Date objects.*/
const DATE_DAY_FORMATTER : String = "D";
/** Full year, e.g. 2007 on Date objects.*/
const DATE_FULLYEAR_FORMATTER : String = "Y";
/** Year, e.g. 07 on Date objects.*/
const DATE_YEAR_FORMATTER : String = "y";
/** Month from 1 to 12 on Date objects.*/
const DATE_MONTH_FORMATTER : String = "m";
/** Hours (0-23) on Date objects.*/
const DATE_HOUR24_FORMATTER : String = "H";
/** Hours 0-12 on Date objects.*/
const DATE_HOUR_FORMATTER : String = "I";
/** a.m or p.m on Date objects.*/
const DATE_HOUR_AMPM_FORMATTER : String = "p";
/** Minutes on Date objects.*/
const DATE_MINUTES_FORMATTER : String = "M";
/** Seconds on Date objects.*/
const DATE_SECONDS_FORMATTER : String = "S";
/** A string rep of a Date object on the current locale.*/
const DATE_TOLOCALE_FORMATTER : String = "c";

var version : String = "$Id$";

/** @private
 * Internal class that normalizes matching information.
 */
class Match{
    public var startIndex : int;
    public var endIndex : int;
    public var length : int;
    public var content : String;
    public var replacement : String;
    public var before : String;
    public function toString() : String{
        return "Match [" + startIndex + " - " + endIndex + "] (" + length + ") " + content + ", replacement:" + replacement + ";";
    }
}

/** @private */
function padString(str:String, paddingNum:int, paddingChar:String=" ") : String
{
    if(paddingChar == null) return str;

    return new Array(paddingNum + 1).join(paddingChar).concat(str);
}

package
{
        /**
        *   Creates a string with variable substitutions. Very similiar to printf, specially python's printf
        *   @param raw The string to be substituted.
        *   @param rest  The objects to be substitued, can be positional or by properties inside the object (in wich case only one object can be passed)
        *   @return The formated and substitued string.
        *   @example
        *

        *
        *   // objects are substitued in the other they appear
        *
        *   printf("This is an %s lybrary for creating %s", "Actioscript 3.0", "strings");
        *   // outputs: "This is an Actioscript 3.0 lybrary for creating strings";
        *   // you can also format numbers:
        *
        *   printf("You can also display numbers like PI: %f, and format them to a fixed precision, such as PI with 3 decimal places %.3f", Math.PI, Math.PI);
        *   // outputs: " You can also display numbers like PI: 3.141592653589793, and format them to a fixed precision, such as PI with 3 decimal places 3.142"
        *   // Instead of positional (the order of arguments to print f, you can also use propertie of an object):
        *   var userInfo : Object = {
        	"name": "Arthur Debert",
        	"email": "arthur@stimuli.com.br",
        	"website":"http://www.stimuli.com.br/",
        	"ocupation": "developer"
        }
        *
        *   printf("My name is %(name)s and I am a %(ocupation)s. You can read more on my personal %(website)s, or reach me through my %(email)s", userInfo);
        *   // outputs: "My name is Arthur Debert and I am a developer. You can read more on my personal http://www.stimuli.com.br/, or reach me through my arthur@stimuli.com.br"
        *   // you can also use date parts:
        *   var date : Date = new Date();
        *   printf("Today is %d/%m/%Y", date, date, date)
        *
        *

        *   @see br.com.stimuli.string
        */
         public function printf(raw : String, ...rest) : String{
         	/**
			* Pretty ugly!
			*   basicaly
			*   % -> the start of a substitution hole
			*   (some_var_name) -> [optional] used in named substitutions
			*   .xx -> [optional] the precision with witch numbers will be formated
			*   x -> the formatter (string, hexa, float, date part)
			*/
            var SUBS_RE : RegExp = /%(?!^%)(\((?P<var_name>[\w]+[\w_\d]+)\))?(?P<padding>[0-9]{1,2})?(\.(?P<precision>[0-9]+))?(?P<formater>[sxofaAbBcdHIjmMpSUwWxXyYZ])/ig;

			//Return empty string if raw is null, we don't want errors here
			if( raw == null ){
				return "";
			}
			//trace("\n\n" + 'input:"'+ raw+  '" , args:', rest.join(", ")) ;
            var matches : Array = [];
            var result : Object = SUBS_RE.exec(raw);
            var match : Match;
            var runs : int = 0;
            var numMatches : int = 0;
            var numberVariables : int = rest.length;
            // quick check if we find string subs amongst the text to match (something like %(foo)s
            var isPositionalSubts : Boolean = !Boolean(raw.match(/%\(\s*[\w\d_]+\s*\)/));
            var replacementValue : *;
            var formater : String;
            var varName : String;
            var precision : String;
            var padding : String;
            var paddingNum : int;
            var paddingChar:String;

            // matched through the string, creating Match objects for easier later reuse
            while (Boolean(result)){
                match = new Match();
                match.startIndex = result.index;
                match.length = String(result[0]).length;
                match.endIndex = match.startIndex + match.length;
                match.content = String(result[0]);
                // try to get substitution properties
                formater = result["formater"];
                varName = result["var_name"];
                precision = result["precision"];
                padding = result["padding"];
				paddingNum = 0;
				paddingChar = null;
                //trace('formater:', formater, ', varName:', varName, ', precision:', precision, 'padding:', padding);
                if (padding){
                    if (padding.length == 1){
                        paddingNum = int(padding);
                        paddingChar = " ";
                    }else{
                        paddingNum = int (padding.substr(-1, 1));
                        paddingChar = padding.substr(-2, 1);
                        if (paddingChar != "0"){
                            paddingNum *= int(paddingChar);
                            paddingChar = " ";
                        }
                    }
                }
                if (isPositionalSubts){
                    // by position, grab next subs:
                     replacementValue = rest[matches.length];

                }else{
                    // be hash / properties
                    replacementValue = rest[0] == null ? undefined : rest[0][varName];
                }
                // check for bad variable names
                if (replacementValue == undefined){
                   replacementValue = "";
                }
                if (replacementValue != undefined){

	                // format the string accodingly to the formatter
	                if (formater == STRING_FORMATTER){
	                    match.replacement = padString(String(replacementValue), paddingNum, paddingChar);
	                }else if (formater == FLOAT_FORMATER){
	                    // floats, check if we need to truncate precision
	                    if (precision){
	                        match.replacement = padString(
	                                        Number(replacementValue).toFixed( int(precision)),
	                                        paddingNum,
	                                        paddingChar);
	                    }else{
	                        match.replacement = padString(String(replacementValue), paddingNum, paddingChar);
	                    }
	                }else if (formater == INTEGER_FORMATER){
	                    match.replacement = padString(int(replacementValue).toString(), paddingNum, paddingChar);
	                }else if (formater == OCTAL_FORMATER){
	                    match.replacement = "0" + int(replacementValue).toString(8);
	                }else if (formater == HEXA_FORMATER){
	                    match.replacement = "0x" + int(replacementValue).toString(16);
	                }else if(DATES_FORMATERS.indexOf(formater) > -1){
	                    switch (formater){
	                        case DATE_DAY_FORMATTER:
	                            match.replacement = replacementValue["date"];
	                            break;
	                        case DATE_FULLYEAR_FORMATTER:
	                            match.replacement = replacementValue["fullYear"];
	                            break;
	                        case DATE_YEAR_FORMATTER:
	                            match.replacement = String(replacementValue["fullYear"]).substr(2, 2);
	                            break;
	                        case DATE_MONTH_FORMATTER:
	                            match.replacement = String(replacementValue["month"] + 1);
	                            break;
	                        case DATE_HOUR24_FORMATTER:
	                            match.replacement = replacementValue["hours"];
	                            break;
	                        case DATE_HOUR_FORMATTER:
	                            var hours24 : Number = replacementValue["hours"];
	                            match.replacement =  (hours24 -12).toString();
	                            break;
	                        case DATE_HOUR_AMPM_FORMATTER:
	                            match.replacement =  (replacementValue["hours"]  >= 12 ? "p.m" : "a.m");
	                            break;
	                        case DATE_TOLOCALE_FORMATTER:
	                            match.replacement = Date(replacementValue).toLocaleString();
	                            break;
	                        case DATE_MINUTES_FORMATTER:
	                            match.replacement = replacementValue["minutes"];
	                            break;
	                        case DATE_SECONDS_FORMATTER:
	                            match.replacement = replacementValue["seconds"];
	                            break;
	                    }
	                }else{
	                   //no good replacement
	                }
               		matches.push(match);
                }

                // just a small check in case we get stuck: kludge!
                runs ++;
                if (runs > 10000){
                   //something is wrong, breaking out
                    break;
                }
                numMatches ++;
                // iterates next match
                result = SUBS_RE.exec(raw);
            }
            // in case there's nothing to substitute, just return the initial string
            if(matches.length == 0){
                //no matches, returning raw
               return raw;
            }
            // now actually do the substitution, keeping a buffer to be joined at
            //the end for better performance
            var buffer : Array = [];
            var lastMatch : Match;
            // beggininf os string, if it doesn't start with a substitition
            var previous : String = raw.substr(0, matches[0].startIndex);
            var subs : String;
            for each(match in matches){
                // finds out the previous string part and the next substitition
                if (lastMatch){
                    previous = raw.substring(lastMatch.endIndex  ,  match.startIndex);
                }
                buffer.push(previous);
                buffer.push(match.replacement);
                lastMatch = match;

            }
            // buffer the tail of the string: text after the last substitution
            buffer.push(raw.substr(match.endIndex, raw.length - match.endIndex));
            //trace('returning: "'+ buffer.join("")+'"');
            return buffer.join("");
        }
    }

// internal usage
/** @private */
const BAD_VARIABLE_NUMBER : String = "The number of variables to be replaced and template holes don't match";
/** Converts to a string*/
const STRING_FORMATTER : String = "s";
/** Outputs as a Number, can use the precision specifier: %.2sf will output a float with 2 decimal digits.*/
const FLOAT_FORMATER : String = "f";
/** Outputs as an Integer.*/
const INTEGER_FORMATER : String = "d";
/** Converts to an OCTAL number */
const OCTAL_FORMATER : String = "o";
/** Converts to a Hexa number (includes 0x) */
const HEXA_FORMATER : String = "x";
/** @private */
const DATES_FORMATERS : String = "aAbBcDHIjmMpSUwWxXyYZ";
/** Day of month, from 0 to 30 on Date objects.*/
const DATE_DAY_FORMATTER : String = "D";
/** Full year, e.g. 2007 on Date objects.*/
const DATE_FULLYEAR_FORMATTER : String = "Y";
/** Year, e.g. 07 on Date objects.*/
const DATE_YEAR_FORMATTER : String = "y";
/** Month from 1 to 12 on Date objects.*/
const DATE_MONTH_FORMATTER : String = "m";
/** Hours (0-23) on Date objects.*/
const DATE_HOUR24_FORMATTER : String = "H";
/** Hours 0-12 on Date objects.*/
const DATE_HOUR_FORMATTER : String = "I";
/** a.m or p.m on Date objects.*/
const DATE_HOUR_AMPM_FORMATTER : String = "p";
/** Minutes on Date objects.*/
const DATE_MINUTES_FORMATTER : String = "M";
/** Seconds on Date objects.*/
const DATE_SECONDS_FORMATTER : String = "S";
/** A string rep of a Date object on the current locale.*/
const DATE_TOLOCALE_FORMATTER : String = "c";

var version : String = "$Id$";

/** @private
 * Internal class that normalizes matching information.
 */
class Match{
    public var startIndex : int;
    public var endIndex : int;
    public var length : int;
    public var content : String;
    public var replacement : String;
    public var before : String;
    public function toString() : String{
        return "Match [" + startIndex + " - " + endIndex + "] (" + length + ") " + content + ", replacement:" + replacement + ";";
    }
}

/** @private */
function padString(str:String, paddingNum:int, paddingChar:String=" ") : String
{
    if(paddingChar == null) return str;

    return new Array(paddingNum).join(paddingChar).substr(0,paddingNum-str.length).concat(str);
}

fix the padding length

fix the padding length


//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.utils.ByteArray;

import flump.mold.MovieMold;
import flump.xfl.XflLibrary;

import com.threerings.util.Log;

public class Publisher
{
    public function Publisher(exportDir :File, format :Format, ...formats) {
        _exportDir = exportDir;
        _formats.push(format);
        for each (format in formats) _formats.push(format);
    }

    public function modified (lib :XflLibrary) :Boolean {
        const destDir :File = _exportDir.resolvePath(lib.location);
        for each (var format :Format in _formats) {
            var metadata :File = format.getMetadata(destDir);
            if (!metadata.exists) return true;

            var stream :FileStream = new FileStream();
            stream.open(metadata, FileMode.READ);
            var bytes :ByteArray = new ByteArray();
            stream.readBytes(bytes);
            stream.close();

            try {
                if (format.extractMd5(bytes) != lib.md5) return true;
            } catch (e :Error) {
                log.warning("Hit exception parsing existing metadata; calling it modified", e);
                return true;
            }
        }
        return false;
    }

    public function publish (lib :XflLibrary, authoredDevice :DeviceType) :void {
        var packers :Vector.<Packer> = new <Packer>[
            new Packer(DeviceType.IPHONE_RETINA, authoredDevice, lib),
            new Packer(DeviceType.IPHONE, authoredDevice, lib)
        ];

        const destDir :File = _exportDir.resolvePath(lib.location);
        destDir.createDirectory();

        for each (var packer :Packer in packers) {
            for each (var atlas :Atlas in packer.atlases) {
                atlas.publish(_exportDir);
            }
        }
        const symbolMovies :Vector.<MovieMold> = lib.movies.filter(
            function (movie :MovieMold, ..._) :Boolean { return movie.symbol != null });

        for each (var format :Format in _formats) {
            var out :FileStream = new FileStream();
            out.open(format.getMetadata(destDir), FileMode.WRITE);

            format.publish(out, lib, symbolMovies, packers, authoredDevice);
            out.close();
        }
    }

    private var _exportDir :File;
    private const _formats :Vector.<Format> = new Vector.<Format>();
    private static const log :Log = Log.getLog(Publisher);
}
}

//
// Flump - Copyright 2012 Three Rings Design

package flump.export {

import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.utils.ByteArray;

import flump.mold.MovieMold;
import flump.xfl.XflLibrary;

import com.threerings.util.Log;

public class Publisher
{
    public function Publisher(exportDir :File, format :Format, ...formats) {
        _exportDir = exportDir;
        _formats.push(format);
        for each (format in formats) _formats.push(format);
    }

    public function modified (lib :XflLibrary) :Boolean {
        const destDir :File = _exportDir.resolvePath(lib.location);
        for each (var format :Format in _formats) {
            var metadata :File = format.getMetadata(destDir);
            if (!metadata.exists) return true;

            var stream :FileStream = new FileStream();
            stream.open(metadata, FileMode.READ);
            var bytes :ByteArray = new ByteArray();
            stream.readBytes(bytes);
            stream.close();

            try {
                if (format.extractMd5(bytes) != lib.md5) return true;
            } catch (e :Error) {
                log.warning("Hit exception parsing existing metadata; calling it modified", e);
                return true;
            }
        }
        return false;
    }

    public function publish (lib :XflLibrary, authoredDevice :DeviceType) :void {
        var packers :Vector.<Packer> = new <Packer>[
            new Packer(DeviceType.IPHONE_RETINA, authoredDevice, lib),
            new Packer(DeviceType.IPHONE, authoredDevice, lib)
        ];

        const destDir :File = _exportDir.resolvePath(lib.location);
        destDir.createDirectory();

        // Ensure any previously generated atlases don't linger
        for each (var file :File in destDir.getDirectoryListing()) {
            if (file.name.match(/atlas.*\.png/)) file.deleteFile();
        }

        for each (var packer :Packer in packers) {
            for each (var atlas :Atlas in packer.atlases) {
                atlas.publish(_exportDir);
            }
        }
        const symbolMovies :Vector.<MovieMold> = lib.movies.filter(
            function (movie :MovieMold, ..._) :Boolean { return movie.symbol != null });

        for each (var format :Format in _formats) {
            var out :FileStream = new FileStream();
            out.open(format.getMetadata(destDir), FileMode.WRITE);

            format.publish(out, lib, symbolMovies, packers, authoredDevice);
            out.close();
        }
    }

    private var _exportDir :File;
    private const _formats :Vector.<Format> = new Vector.<Format>();
    private static const log :Log = Log.getLog(Publisher);
}
}

Remove old atlases before publishing.

Remove old atlases before publishing.

Previously, if a movie was changed to use fewer atlases and re-exported, old
atlases would still be around.


package net.flashpunk.utils
{
	import flash.display.Stage;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.ui.Keyboard;
	import net.flashpunk.*;

	/**
	 * Static class updated by Engine. Use for defining and checking keyboard/mouse input.
	 */
	public class Input
	{
		/**
		 * An updated string containing the last 100 characters pressed on the keyboard.
		 * Useful for creating text input fields, such as highscore entries, etc.
		 */
		public static var keyString:String = "";

		/**
		 * If the mouse button is down.
		 */
		public static var mouseDown:Boolean = false;

		/**
		 * If the mouse button is up.
		 */
		public static var mouseUp:Boolean = true;

		/**
		 * If the mouse button was pressed this frame.
		 */
		public static var mousePressed:Boolean = false;

		/**
		 * If the mouse button was released this frame.
		 */
		public static var mouseReleased:Boolean = false;

		/**
		 * X position of the mouse on the screen.
		 */
		public static function get mouseX():int
		{
			return FP.screen.mouseX;
		}

		/**
		 * Y position of the mouse on the screen.
		 */
		public static function get mouseY():int
		{
			return FP.screen.mouseY;
		}

		/**
		 * Defines a new input.
		 * @param	name		String to map the input to.
		 * @param	...keys		The keys to use for the Input.
		 */
		public static function define(name:String, ...keys):void
		{
			_control[name] = Vector.<int>(keys);
		}

		/**
		 * If the input or key is held down.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function check(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if (v[i] < 0)
					{
						if (_keyNum > 0) return true;
						continue;
					}
					if (_key[v[i]]) return true;
				}
				return false;
			}
			return input < 0 ? _keyNum > 0 : _key[input];
		}

		/**
		 * If the input or key was pressed this frame.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function pressed(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if ((v[i] < 0 && _press.length) || _press.indexOf(v[i]) >= 0) return true;
				}
				return false;
			}
			return (input < 0 && _press.length) || _press.indexOf(input) >= 0;
		}

		/**
		 * If the input or key was released this frame.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function released(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if ((v[i] < 0 && _release.length) || _release.indexOf(v[i]) >= 0) return true;
				}
				return false;
			}
			return (input < 0 && _release.length) || _release.indexOf(input) >= 0;
		}

		/**
		 * Returns the keys mapped to the input name.
		 * @param	name		The input name.
		 * @return	A Vector of keys.
		 */
		public static function keys(name:String):Vector.<int>
		{
			return _control[name] as Vector.<int>;
		}

		/** @private Called by Engine to enable keyboard input on the stage. */
		public static function enable():void
		{
			if (!_enabled && FP.stage)
			{
				FP.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
				FP.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
				FP.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				FP.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
				_enabled = true;
			}
		}

		/** @private Called by Engine to update the input. */
		public static function update():void
		{
			while (_pressNum --) _press[_pressNum] = -1;
			_pressNum = 0;
			while (_releaseNum --) _release[_releaseNum] = -1;
			_releaseNum = 0;
			if (mousePressed) mousePressed = false;
			if (mouseReleased) mouseReleased = false;
		}

		/** @private Event handler for key press. */
		private static function onKeyDown(e:KeyboardEvent):void
		{
			// get the keycode
			var code:int = e.keyCode;

			// update the keystring
			if (code == Key.BACKSPACE) keyString = keyString.substring(0, keyString.length - 1);
			else if ((code > 47 && code < 58) || (code > 64 && code < 91) || code == 32)
			{
				if (keyString.length > KEYSTRING_MAX) keyString = keyString.substring(1);
				var char:String = String.fromCharCode(code);
				if (e.shiftKey || Keyboard.capsLock) char = char.toLocaleUpperCase();
				else char = char.toLocaleLowerCase();
				keyString += char;
			}

			// update the keystate
			if (!_key[code])
			{
				_key[code] = true;
				_keyNum ++;
				_press[_pressNum ++] = code;
			}
		}

		/** @private Event handler for key release. */
		private static function onKeyUp(e:KeyboardEvent):void
		{
			// get the keycode and update the keystate
			var code:int = e.keyCode;
			if (_key[code])
			{
				_key[code] = false;
				_keyNum --;
				_release[_releaseNum ++] = code;
			}
		}

		/** @private Event handler for mouse press. */
		private static function onMouseDown(e:MouseEvent):void
		{
			if (!mouseDown)
			{
				mouseDown = true;
				mouseUp = false;
				mousePressed = true;
				mouseReleased = false;
			}
		}

		/** @private Event handler for mouse release. */
		private static function onMouseUp(e:MouseEvent):void
		{
			mouseDown = false;
			mouseUp = true;
			mousePressed = false;
			mouseReleased = true;
		}

		// Max amount of characters stored by the keystring.
		/** @private */ private static const KEYSTRING_MAX:uint = 100;

		// Input information.
		/** @private */ private static var _enabled:Boolean = false;
		/** @private */ private static var _key:Vector.<Boolean> = new Vector.<Boolean>(256);
		/** @private */ private static var _keyNum:int = 0;
		/** @private */ private static var _press:Vector.<int> = new Vector.<int>(256);
		/** @private */ private static var _release:Vector.<int> = new Vector.<int>(256);
		/** @private */ private static var _pressNum:int = 0;
		/** @private */ private static var _releaseNum:int = 0;
		/** @private */ private static var _control:Array = [];
	}
}

package net.flashpunk.utils
{
	import flash.display.Stage;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.ui.Keyboard;
	import net.flashpunk.*;

	/**
	 * Static class updated by Engine. Use for defining and checking keyboard/mouse input.
	 */
	public class Input
	{
		/**
		 * An updated string containing the last 100 characters pressed on the keyboard.
		 * Useful for creating text input fields, such as highscore entries, etc.
		 */
		public static var keyString:String = "";

		/**
		 * If the mouse button is down.
		 */
		public static var mouseDown:Boolean = false;

		/**
		 * If the mouse button is up.
		 */
		public static var mouseUp:Boolean = true;

		/**
		 * If the mouse button was pressed this frame.
		 */
		public static var mousePressed:Boolean = false;

		/**
		 * If the mouse button was released this frame.
		 */
		public static var mouseReleased:Boolean = false;

		/**
		 * If the mouse wheel was moved this frame.
		 */
		public static var mouseWheel:Boolean = false;

		/**
		 * If the mouse wheel was moved this frame, this was the delta.
		 */
		public static function get mouseWheelDelta():int
		{
		    if (mouseWheel)
		    {
		        mouseWheel = false;
		        return _mouseWheelDelta;
		    } else {
		        return 0;
		    }
		}

		/**
		 * X position of the mouse on the screen.
		 */
		public static function get mouseX():int
		{
			return FP.screen.mouseX;
		}

		/**
		 * Y position of the mouse on the screen.
		 */
		public static function get mouseY():int
		{
			return FP.screen.mouseY;
		}

		/**
		 * Defines a new input.
		 * @param	name		String to map the input to.
		 * @param	...keys		The keys to use for the Input.
		 */
		public static function define(name:String, ...keys):void
		{
			_control[name] = Vector.<int>(keys);
		}

		/**
		 * If the input or key is held down.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function check(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if (v[i] < 0)
					{
						if (_keyNum > 0) return true;
						continue;
					}
					if (_key[v[i]]) return true;
				}
				return false;
			}
			return input < 0 ? _keyNum > 0 : _key[input];
		}

		/**
		 * If the input or key was pressed this frame.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function pressed(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if ((v[i] < 0 && _press.length) || _press.indexOf(v[i]) >= 0) return true;
				}
				return false;
			}
			return (input < 0 && _press.length) || _press.indexOf(input) >= 0;
		}

		/**
		 * If the input or key was released this frame.
		 * @param	input		An input name or key to check for.
		 * @return	True or false.
		 */
		public static function released(input:*):Boolean
		{
			if (input is String)
			{
				var v:Vector.<int> = _control[input],
					i:int = v.length;
				while (i --)
				{
					if ((v[i] < 0 && _release.length) || _release.indexOf(v[i]) >= 0) return true;
				}
				return false;
			}
			return (input < 0 && _release.length) || _release.indexOf(input) >= 0;
		}

		/**
		 * Returns the keys mapped to the input name.
		 * @param	name		The input name.
		 * @return	A Vector of keys.
		 */
		public static function keys(name:String):Vector.<int>
		{
			return _control[name] as Vector.<int>;
		}

		/** @private Called by Engine to enable keyboard input on the stage. */
		public static function enable():void
		{
			if (!_enabled && FP.stage)
			{
				FP.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
				FP.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
				FP.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				FP.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
				FP.stage.addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel);
				_enabled = true;
			}
		}

		/** @private Called by Engine to update the input. */
		public static function update():void
		{
			while (_pressNum --) _press[_pressNum] = -1;
			_pressNum = 0;
			while (_releaseNum --) _release[_releaseNum] = -1;
			_releaseNum = 0;
			if (mousePressed) mousePressed = false;
			if (mouseReleased) mouseReleased = false;
		}

		/** @private Event handler for key press. */
		private static function onKeyDown(e:KeyboardEvent):void
		{
			// get the keycode
			var code:int = e.keyCode;

			// update the keystring
			if (code == Key.BACKSPACE) keyString = keyString.substring(0, keyString.length - 1);
			else if ((code > 47 && code < 58) || (code > 64 && code < 91) || code == 32)
			{
				if (keyString.length > KEYSTRING_MAX) keyString = keyString.substring(1);
				var char:String = String.fromCharCode(code);
				if (e.shiftKey || Keyboard.capsLock) char = char.toLocaleUpperCase();
				else char = char.toLocaleLowerCase();
				keyString += char;
			}

			// update the keystate
			if (!_key[code])
			{
				_key[code] = true;
				_keyNum ++;
				_press[_pressNum ++] = code;
			}
		}

		/** @private Event handler for key release. */
		private static function onKeyUp(e:KeyboardEvent):void
		{
			// get the keycode and update the keystate
			var code:int = e.keyCode;
			if (_key[code])
			{
				_key[code] = false;
				_keyNum --;
				_release[_releaseNum ++] = code;
			}
		}

		/** @private Event handler for mouse press. */
		private static function onMouseDown(e:MouseEvent):void
		{
			if (!mouseDown)
			{
				mouseDown = true;
				mouseUp = false;
				mousePressed = true;
				mouseReleased = false;
			}
		}

		/** @private Event handler for mouse release. */
		private static function onMouseUp(e:MouseEvent):void
		{
			mouseDown = false;
			mouseUp = true;
			mousePressed = false;
			mouseReleased = true;
		}

		/** @private Event handler for mouse wheel events */
		private static function onMouseWheel(e:MouseEvent):void
		{
		    mouseWheel = true;
		    _mouseWheelDelta = e.delta;
		}

		// Max amount of characters stored by the keystring.
		/** @private */ private static const KEYSTRING_MAX:uint = 100;

		// Input information.
		/** @private */ private static var _enabled:Boolean = false;
		/** @private */ private static var _key:Vector.<Boolean> = new Vector.<Boolean>(256);
		/** @private */ private static var _keyNum:int = 0;
		/** @private */ private static var _press:Vector.<int> = new Vector.<int>(256);
		/** @private */ private static var _release:Vector.<int> = new Vector.<int>(256);
		/** @private */ private static var _pressNum:int = 0;
		/** @private */ private static var _releaseNum:int = 0;
		/** @private */ private static var _control:Array = [];
		/** @private */ private static var _mouseWheelDelta:int = 0;
	}
}

add rudimentary support for mouse eheel events in Input system