menu.addMenuItem(Commands.FILE_NEW,                 "Ctrl-N");

menu.addMenuItem(Commands.FILE_OPEN,                "Ctrl-O");

menu.addMenuItem(Commands.FILE_CLOSE,               "Ctrl-W");

menu.addMenuItem(Commands.FILE_SAVE,                "Ctrl-S");

menu.addMenuItem(Commands.FILE_SAVE_ALL,            "Ctrl-Alt-S");

menu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW,   "Ctrl-Alt-P");

keep keep keep keep add keep keep keep keep keep

menu.addMenuItem(Commands.FILE_CLOSE_ALL,           "Ctrl-Shift-W");

/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, PathUtils, window */

define(function (require, exports, module) {
    "use strict";

    require("thirdparty/path-utils/path-utils.min");

    // Load dependent modules
    var CommandManager      = require("command/CommandManager"),
        Commands            = require("command/Commands"),
        KeyBindingManager   = require("command/KeyBindingManager"),
        NativeFileSystem    = require("file/NativeFileSystem").NativeFileSystem,
        ProjectManager      = require("project/ProjectManager"),
        DocumentManager     = require("document/DocumentManager"),
        EditorManager       = require("editor/EditorManager"),
        FileViewController  = require("project/FileViewController"),
        FileUtils           = require("file/FileUtils"),
        StringUtils         = require("utils/StringUtils"),
        Async               = require("utils/Async"),
        Dialogs             = require("widgets/Dialogs"),
        Strings             = require("strings"),
        PreferencesManager  = require("preferences/PreferencesManager"),
        PerfUtils           = require("utils/PerfUtils"),
        KeyEvent            = require("utils/KeyEvent");

    /**
     * Handlers for commands related to document handling (opening, saving, etc.)
     */

    /** @type {jQueryObject} Container for label shown above editor; must be an inline element */
    var _$title = null;
    /** @type {jQueryObject} Container for dirty dot; must be an inline element */
    var _$dirtydot = null;
    /** @type {jQueryObject} Container for _$title; need not be an inline element */
    var _$titleWrapper = null;
    /** @type {string} Label shown above editor for current document: filename and potentially some of its path */
    var _currentTitlePath = null;

    /** @type {jQueryObject} Container for _$titleWrapper; if changing title changes this element's height, must kick editor to resize */
    var _$titleContainerToolbar = null;
    /** @type {Number} Last known height of _$titleContainerToolbar */
    var _lastToolbarHeight = null;

    function updateTitle() {
        var currentDoc = DocumentManager.getCurrentDocument();
        if (currentDoc) {
            _$title.text(_currentTitlePath);
            _$title.attr("title", currentDoc.file.fullPath);
            // dirty dot is always in DOM so layout doesn't change, and visibility is toggled
            _$dirtydot.css("visibility", (currentDoc.isDirty) ? "visible" : "hidden");
        } else {
            _$title.text("");
            _$title.attr("title", "");
            _$dirtydot.css("visibility", "hidden");
        }

        // Set _$titleWrapper to a fixed width just large enough to accomodate _$title. This seems equivalent to what
        // the browser would do automatically, but the CSS trick we use for layout requires _$titleWrapper to have a
        // fixed width set on it (see the "#main-toolbar.toolbar" CSS rule for details).
        _$titleWrapper.css("width", "");
        var newWidth = _$title.width();
        _$titleWrapper.css("width", newWidth);

        // Changing the width of the title may cause the toolbar layout to change height, which needs to resize the
        // editor beneath it (toolbar changing height due to window resize is already caught by EditorManager).
        var newToolbarHeight = _$titleContainerToolbar.height();
        if (_lastToolbarHeight !== newToolbarHeight) {
            _lastToolbarHeight = newToolbarHeight;
            EditorManager.resizeEditor();
        }
    }

    function updateDocumentTitle() {
        var newDocument = DocumentManager.getCurrentDocument();

        // TODO: This timer is causing a "Recursive tests with the same name are not supporte"
        // exception. This code should be removed (if not needed), or updated with a unique
        // timer name (if needed).
        // var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath));

        if (newDocument) {
            var fullPath = newDocument.file.fullPath;

            // In the main toolbar, show the project-relative path (if the file is inside the current project)
            // or the full absolute path (if it's not in the project).
            _currentTitlePath = ProjectManager.makeProjectRelativeIfPossible(fullPath);

        } else {
            _currentTitlePath = null;
        }

        // Update title text & "dirty dot" display
        updateTitle();

        // PerfUtils.addMeasurement(perfTimerName);
    }

    function handleDirtyChange(event, changedDoc) {
        var currentDoc = DocumentManager.getCurrentDocument();

        if (currentDoc && changedDoc.file.fullPath === currentDoc.file.fullPath) {
            updateTitle();
        }
    }

    /**
     * @private
     * Creates a document and displays an editor for the specified file path.
     * @param {!string} fullPath
     * @return {$.Promise} a jQuery promise that will be resolved with a
     *  document for the specified file path, or rejected if the file can not be read.
     */
    function doOpen(fullPath) {
        var result = new $.Deferred();

        if (!fullPath) {
            console.log("doOpen() called without fullPath");
            result.reject();
        } else {
            var perfTimerName = PerfUtils.markStart("Open File:\t" + fullPath);
            result.always(function () {
                PerfUtils.addMeasurement(perfTimerName);
            });

            // Load the file if it was never open before, and then switch to it in the UI
            DocumentManager.getDocumentForPath(fullPath)
                .done(function (doc) {
                    DocumentManager.setCurrentDocument(doc);
                    result.resolve(doc);
                })
                .fail(function (fileError) {
                    FileUtils.showFileOpenError(fileError.code, fullPath).done(function () {
                        // For performance, we do lazy checking of file existence, so it may be in working set
                        DocumentManager.removeFromWorkingSet(new NativeFileSystem.FileEntry(fullPath));
                        EditorManager.focusEditor();
                        result.reject();
                    });
                });
        }

        return result.promise();
    }

    /**
     * @private
     * Used to track the default directory for the file open dialog
     */
    var _defaultOpenDialogFullPath = null;

    /**
     * @private
     * Creates a document and displays an editor for the specified file path.
     * If no path is specified, a file prompt is provided for input.
     * @param {?string} fullPath - The path of the file to open; if it's null we'll prompt for it
     * @return {$.Promise} a jQuery promise that will be resolved with a new
     *  document for the specified file path, or rejected if the file can not be read.
     */
    function _doOpenWithOptionalPath(fullPath) {
        var result;
        if (!fullPath) {
            // Create placeholder deferred
            result = new $.Deferred();

            //first time through, default to the current project path
            if (!_defaultOpenDialogFullPath) {
                _defaultOpenDialogFullPath = ProjectManager.getProjectRoot().fullPath;
            }
            // Prompt the user with a dialog
            NativeFileSystem.showOpenDialog(true, false, Strings.OPEN_FILE, _defaultOpenDialogFullPath,
                null, function (paths) {
                    var i;

                    if (paths.length > 0) {
                        // Add all files to the working set without verifying that
                        // they still exist on disk (for faster opening)
                        var filesToOpen = [];
                        paths.forEach(function (file) {
                            filesToOpen.push(new NativeFileSystem.FileEntry(file));
                        });
                        DocumentManager.addListToWorkingSet(filesToOpen);

                        doOpen(paths[paths.length - 1])
                            .done(function (doc) {
                                var url = PathUtils.parseUrl(doc.file.fullPath);
                                //reconstruct the url but use the directory and stop there
                                _defaultOpenDialogFullPath = url.protocol + url.doubleSlash + url.authority + url.directory;

                                DocumentManager.addToWorkingSet(doc.file);
                            })
                            // Send the resulting document that was opened
                            .pipe(result.resolve, result.reject);
                    } else {
                        // Reject if the user canceled the dialog
                        result.reject();
                    }
                });
        } else {
            result = doOpen(fullPath);
        }

        return result.promise();
    }

    /**
     * Opens the given file and makes it the current document. Does NOT add it to the working set.
     * @param {!{fullPath:string}} Params for FILE_OPEN command
     */
    function handleFileOpen(commandData) {
        var fullPath = null;
        if (commandData) {
            fullPath = commandData.fullPath;
        }

        return _doOpenWithOptionalPath(fullPath)
            .always(EditorManager.focusEditor);
    }

    /**
     * Opens the given file, makes it the current document, AND adds it to the working set.
     * @param {!{fullPath:string}} Params for FILE_OPEN command
     */
    function handleFileAddToWorkingSet(commandData) {
        return handleFileOpen(commandData).done(function (doc) {
            // addToWorkingSet is synchronous
            DocumentManager.addToWorkingSet(doc.file);
        });
    }

    /**
     * @private
     * Ensures the suggested file name doesn't already exit.
     * @param {string} dir  The directory to use
     * @param {string} baseFileName  The base to start with, "-n" will get appened to make unique
     * @param {string} fileExt  The file extension
     * @param {boolean} isFolder True if the suggestion is for a folder name
     * @return {$.Promise} a jQuery promise that will be resolved with a unique name starting with
     *   the given base name
     */
    function _getUntitledFileSuggestion(dir, baseFileName, fileExt, isFolder) {
        var result = new $.Deferred();
        var suggestedName = baseFileName + fileExt;
        var dirEntry = new NativeFileSystem.DirectoryEntry(dir);

        result.progress(function attemptNewName(suggestedName, nextIndexToUse) {
            if (nextIndexToUse > 99) {
                //we've tried this enough
                result.reject();
                return;
            }

            //check this name
            var successCallback = function (entry) {
                //file exists, notify to the next progress
                result.notify(baseFileName + "-" + nextIndexToUse + fileExt, nextIndexToUse + 1);
            };
            var errorCallback = function (error) {
                //most likely error is FNF, user is better equiped to handle the rest
                result.resolve(suggestedName);
            };

            if (isFolder) {
                dirEntry.getDirectory(
                    suggestedName,
                    {},
                    successCallback,
                    errorCallback
                );
            } else {
                dirEntry.getFile(
                    suggestedName,
                    {},
                    successCallback,
                    errorCallback
                );
            }
        });

        //kick it off
        result.notify(baseFileName + fileExt, 1);

        return result.promise();
    }

    /**
     * Prevents re-entrancy into handleFileNewInProject()
     *
     * handleFileNewInProject() first prompts the user to name a file and then asynchronously writes the file when the
     * filename field loses focus. This boolean prevent additional calls to handleFileNewInProject() when an existing
     * file creation call is outstanding
     */
    var fileNewInProgress = false;

    /**
     * Bottleneck function for creating new files and folders in the project tree.
     */
    function _handleNewItemInProject(isFolder) {
        if (fileNewInProgress) {
            ProjectManager.forceFinishRename();
            return;
        }
        fileNewInProgress = true;

        // Determine the directory to put the new file
        // If a file is currently selected, put it next to it.
        // If a directory is currently selected, put it in it.
        // If nothing is selected, put it at the root of the project
        var baseDir,
            selected = ProjectManager.getSelectedItem() || ProjectManager.getProjectRoot();

        baseDir = selected.fullPath;
        if (selected.isFile) {
            baseDir = baseDir.substr(0, baseDir.lastIndexOf("/"));
        }

        // Create the new node. The createNewItem function does all the heavy work
        // of validating file name, creating the new file and selecting.
        var deferred = _getUntitledFileSuggestion(baseDir, Strings.UNTITLED, isFolder ? "" : ".js", isFolder);
        var createWithSuggestedName = function (suggestedName) {
            ProjectManager.createNewItem(baseDir, suggestedName, false, isFolder)
                .pipe(deferred.resolve, deferred.reject, deferred.notify)
                .always(function () { fileNewInProgress = false; });
        };

        deferred.done(createWithSuggestedName);
        deferred.fail(function createWithDefault() { createWithSuggestedName(isFolder ? "Untitled" : "Untitled.js"); });
        return deferred;
    }

    /**
     * Create a new file in the project tree.
     */
    function handleFileNewInProject() {
        _handleNewItemInProject(false);
    }

    /**
     * Create a new folder in the project tree.
     */
    function handleNewFolderInProject() {
        _handleNewItemInProject(true);
    }

    function showSaveFileError(code, path) {
        return Dialogs.showModalDialog(
            Dialogs.DIALOG_ID_ERROR,
            Strings.ERROR_SAVING_FILE_TITLE,
            StringUtils.format(
                Strings.ERROR_SAVING_FILE,
                StringUtils.htmlEscape(path),
                FileUtils.getFileErrorString(code)
            )
        );
    }

    /** Note: if there is an error, the promise is not rejected until the user has dimissed the dialog */
    function doSave(docToSave) {
        var result = new $.Deferred();

        function handleError(error, fileEntry) {
            showSaveFileError(error.code, fileEntry.fullPath)
                .always(function () {
                    result.reject(error);
                });
        }

        if (docToSave && docToSave.isDirty) {
            var fileEntry = docToSave.file;
            var writeError = false;

            fileEntry.createWriter(
                function (writer) {
                    writer.onwriteend = function () {
                        // Per spec, onwriteend is called after onerror too
                        if (!writeError) {
                            docToSave.notifySaved();
                            result.resolve();
                        }
                    };
                    writer.onerror = function (error) {
                        writeError = true;
                        handleError(error, fileEntry);
                    };

                    // We don't want normalized line endings, so it's important to pass true to getText()
                    writer.write(docToSave.getText(true));
                },
                function (error) {
                    handleError(error, fileEntry);
                }
            );
        } else {
            result.resolve();
        }
        result.always(function () {
            EditorManager.focusEditor();
        });
        return result.promise();
    }

    /**
     * Saves the given file. If no file specified, assumes the current document.
     * @param {?{doc: Document}} commandData  Document to close, or null
     * @return {$.Promise} a promise that is resolved after the save completes
     */
    function handleFileSave(commandData) {
        // Default to current document if doc is null
        var doc = null;
        if (commandData) {
            doc = commandData.doc;
        }
        if (!doc) {
            var focusedEditor = EditorManager.getFocusedEditor();

            if (focusedEditor) {
                doc = focusedEditor.document;
            }

            // doc may still be null, e.g. if no editors are open, but doSave() does a null check on
            // doc and makes sure the document is dirty before saving.
        }

        return doSave(doc);
    }

    /**
     * Saves all unsaved documents. Returns a Promise that will be resolved once ALL the save
     * operations have been completed. If ANY save operation fails, an error dialog is immediately
     * shown and the other files wait to save until it is dismissed; after all files have been
     * processed, the Promise is rejected if any ONE save operation failed.
     *
     * @return {$.Promise}
     */
    function saveAll() {
        // Do in serial because doSave shows error UI for each file, and we don't want to stack
        // multiple dialogs on top of each other
        return Async.doSequentially(
            DocumentManager.getWorkingSet(),
            function (file) {
                var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
                if (doc) {
                    return doSave(doc);
                } else {
                    // working set entry that was never actually opened - ignore
                    return (new $.Deferred()).resolve().promise();
                }
            },
            false
        );
    }

    /**
     * Saves all unsaved documents.
     * @return {$.Promise} a promise that is resolved once ALL the saves have been completed; or rejected
     *      after all operations completed if any ONE of them failed.
     */
    function handleFileSaveAll() {
        return saveAll();
    }

    /**
     * Reverts the Document to the current contents of its file on disk. Discards any unsaved changes
     * in the Document.
     * @param {Document} doc
     * @return {$.Promise} a Promise that's resolved when done, or rejected with a FileError if the
     *      file cannot be read (after showing an error dialog to the user).
     */
    function doRevert(doc) {
        var result = new $.Deferred();

        FileUtils.readAsText(doc.file)
            .done(function (text, readTimestamp) {
                doc.refreshText(text, readTimestamp);
                result.resolve();
            })
            .fail(function (error) {
                FileUtils.showFileOpenError(error.code, doc.file.fullPath)
                    .always(function () {
                        result.reject(error);
                    });
            });

        return result.promise();
    }

    /**
     * Closes the specified file: removes it from the working set, and closes the main editor if one
     * is open. Prompts user about saving changes first, if document is dirty.
     *
     * @param {?{file: FileEntry, promptOnly:boolean}} commandData  Optional bag of arguments:
     *      file - File to close; assumes the current document if not specified.
     *      promptOnly - If true, only displays the relevant confirmation UI and does NOT actually
     *          close the document. This is useful when chaining file-close together with other user
     *          prompts that may be cancelable.
     * @return {$.Promise} a promise that is resolved when the file is closed, or if no file is open.
     *      FUTURE: should we reject the promise if no file is open?
     */
    function handleFileClose(commandData) {
        // If not specified, file defaults to null; promptOnly defaults to falsy
        var file       = commandData && commandData.file,
            promptOnly = commandData && commandData.promptOnly;

        // utility function for handleFileClose: closes document & removes from working set
        function doClose(file) {
            if (!promptOnly) {
                // This selects a different document if the working set has any other options
                DocumentManager.closeFullEditor(file);

                EditorManager.focusEditor();
            }
        }

        var result = new $.Deferred(), promise = result.promise();

        // Default to current document if doc is null
        if (!file) {
            if (DocumentManager.getCurrentDocument()) {
                file = DocumentManager.getCurrentDocument().file;
            }
        }

        // No-op if called when nothing is open; TODO: (issue #273) should command be grayed out instead?
        if (!file) {
            result.resolve();
            return promise;
        }

        var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);

        if (doc && doc.isDirty) {
            // Document is dirty: prompt to save changes before closing
            var filename = PathUtils.parseUrl(doc.file.fullPath).filename;

            Dialogs.showModalDialog(
                Dialogs.DIALOG_ID_SAVE_CLOSE,
                Strings.SAVE_CLOSE_TITLE,
                StringUtils.format(Strings.SAVE_CLOSE_MESSAGE, StringUtils.htmlEscape(filename))
            ).done(function (id) {
                if (id === Dialogs.DIALOG_BTN_CANCEL) {
                    result.reject();
                } else if (id === Dialogs.DIALOG_BTN_OK) {
                    // "Save" case: wait until we confirm save has succeeded before closing
                    doSave(doc)
                        .done(function () {
                            doClose(file);
                            result.resolve();
                        })
                        .fail(function () {
                            result.reject();
                        });
                } else {
                    // "Don't Save" case: even though we're closing the main editor, other views of
                    // the Document may remain in the UI. So we need to revert the Document to a clean
                    // copy of whatever's on disk.
                    doClose(file);

                    // Only reload from disk if we've executed the Close for real,
                    // *and* if at least one other view still exists
                    if (!promptOnly && DocumentManager.getOpenDocumentForPath(file.fullPath)) {
                        doRevert(doc)
                            .pipe(result.resolve, result.reject);
                    } else {
                        result.resolve();
                    }
                }
            });
            result.always(function () {
                EditorManager.focusEditor();
            });
        } else {
            // File is not open, or IS open but Document not dirty: close immediately
            doClose(file);
            EditorManager.focusEditor();
            result.resolve();
        }
        return promise;
    }

    /**
     * Closes all open documents; equivalent to calling handleFileClose() for each document, except
     * that unsaved changes are confirmed once, in bulk.
     * @param {?{promptOnly: boolean}}  If true, only displays the relevant confirmation UI and does NOT
     *          actually close any documents. This is useful when chaining close-all together with
     *          other user prompts that may be cancelable.
     * @return {$.Promise} a promise that is resolved when all files are closed
     */
    function handleFileCloseAll(commandData) {
        var result = new $.Deferred();

        var unsavedDocs = [];
        DocumentManager.getWorkingSet().forEach(function (file) {
            var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
            if (doc && doc.isDirty) {
                unsavedDocs.push(doc);
            }
        });

        if (unsavedDocs.length === 0) {
            // No unsaved changes, so we can proceed without a prompt
            result.resolve();

        } else if (unsavedDocs.length === 1) {
            // Only one unsaved file: show the usual single-file-close confirmation UI
            var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: commandData.promptOnly };

            handleFileClose(fileCloseArgs).done(function () {
                // still need to close any other, non-unsaved documents
                result.resolve();
            }).fail(function () {
                result.reject();
            });

        } else {
            // Multiple unsaved files: show a single bulk prompt listing all files
            var message = Strings.SAVE_CLOSE_MULTI_MESSAGE;

            message += "<ul>";
            unsavedDocs.forEach(function (doc) {
                message += "<li><span class='dialog-filename'>"
                    + StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath))
                    + "</span></li>";
            });
            message += "</ul>";

            Dialogs.showModalDialog(
                Dialogs.DIALOG_ID_SAVE_CLOSE,
                Strings.SAVE_CLOSE_TITLE,
                message
            ).done(function (id) {
                if (id === Dialogs.DIALOG_BTN_CANCEL) {
                    result.reject();
                } else if (id === Dialogs.DIALOG_BTN_OK) {
                    // Save all unsaved files, then if that succeeds, close all
                    saveAll().done(function () {
                        result.resolve();
                    }).fail(function () {
                        result.reject();
                    });
                } else {
                    // "Don't Save" case--we can just go ahead and close all  files.
                    result.resolve();
                }
            });
        }

        // If all the unsaved-changes confirmations pan out above, then go ahead & close all editors
        // NOTE: this still happens before any done() handlers added by our caller, because jQ
        // guarantees that handlers run in the order they are added.
        result.done(function () {
            if (!commandData || !commandData.promptOnly) {
                DocumentManager.closeAll();
            }
        });

        return result.promise();
    }

    /**
    * @private - tracks our closing state if we get called again
    */
    var _windowGoingAway = false;

    /**
    * @private
    * Common implementation for close/quit/reload which all mostly
    * the same except for the final step
    */
    function _handleWindowGoingAway(commandData, postCloseHandler, failHandler) {
        if (_windowGoingAway) {
            //if we get called back while we're closing, then just return
            return (new $.Deferred()).resolve().promise();
        }

        //prevent the default action of closing the window until we can save all the files
        if (commandData && commandData.evt && commandData.evt.cancelable) {
            commandData.evt.preventDefault();
        }

        return CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true })
            .done(function () {
                _windowGoingAway = true;
                PreferencesManager.savePreferences();
                postCloseHandler();
            })
            .fail(function () {
                _windowGoingAway = false;
                if (failHandler) {
                    failHandler();
                }
            });
    }

    /**
    * @private
    * Implementation for abortQuit callback to reset quit sequence settings
    */
    function _handleAbortQuit() {
        _windowGoingAway = false;
    }

    /** Confirms any unsaved changes, then closes the window */
    function handleFileCloseWindow(commandData) {
        return _handleWindowGoingAway(
            commandData,
            function () {
                window.close();
            },
            function () {
                // if fail, tell the app to abort any pending quit operation.
                // TODO: remove this if statement when we move to the new CEF3 shell
                if (brackets.app.abortQuit) {
                    brackets.app.abortQuit();
                }
            }
        );
    }

    /** Show a textfield to rename whatever is currently selected in the sidebar (working set OR tree) */
    function handleFileRename() {
        // Prefer selected tree item (which could be a folder); else use current file
        var entry = ProjectManager.getSelectedItem();
        if (!entry) {
            var doc = DocumentManager.getCurrentDocument();
            entry = doc && doc.file;
        }
        ProjectManager.renameItemInline(entry);
    }

    /** Closes the window, then quits the app */
    function handleFileQuit(commandData) {
        return _handleWindowGoingAway(
            commandData,
            function () {
                brackets.app.quit();
            },
            function () {
                // if fail, don't exit: user canceled (or asked us to save changes first, but we failed to do so)
                // TODO: remove this if statement when we move to the new CEF3 shell
                if (brackets.app.abortQuit) {
                    brackets.app.abortQuit();
                }
            }
        );
    }

    /** Does a full reload of the browser window */
    function handleFileReload(commandData) {
        return _handleWindowGoingAway(commandData, function () {
            window.location.reload(true);
        });
    }

    /** Are we already listening for a keyup to call detectDocumentNavEnd()? */
    var _addedNavKeyHandler = false;

    /**
     * When the Ctrl key is released, if we were in the middle of a next/prev document navigation
     * sequence, now is the time to end it and update the MRU order. If we allowed the order to update
     * on every next/prev increment, the 1st & 2nd entries would just switch places forever and we'd
     * never get further down the list.
     * @param {jQueryEvent} event Key-up event
     */
    function detectDocumentNavEnd(event) {
        if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {  // Ctrl key
            DocumentManager.finalizeDocumentNavigation();

            _addedNavKeyHandler = false;
            $(window.document.body).off("keyup", detectDocumentNavEnd);
        }
    }

    /** Navigate to the next/previous (MRU) document. Don't update MRU order yet */
    function goNextPrevDoc(inc) {
        var file = DocumentManager.getNextPrevFile(inc);
        if (file) {
            DocumentManager.beginDocumentNavigation();
            CommandManager.execute(Commands.FILE_OPEN, { fullPath: file.fullPath });

            // Listen for ending of Ctrl+Tab sequence
            if (!_addedNavKeyHandler) {
                _addedNavKeyHandler = true;
                $(window.document.body).keyup(detectDocumentNavEnd);
            }
        }
    }

    function handleGoNextDoc() {
        goNextPrevDoc(+1);
    }
    function handleGoPrevDoc() {
        goNextPrevDoc(-1);
    }

    function handleShowInTree() {
        ProjectManager.showInTree(DocumentManager.getCurrentDocument().file);
    }

    function init($titleContainerToolbar) {
        _$titleContainerToolbar = $titleContainerToolbar;
        _$titleWrapper = $(".title-wrapper", _$titleContainerToolbar);
        _$title = $(".title", _$titleWrapper);
        _$dirtydot = $(".dirty-dot", _$titleWrapper);

        // Register global commands
        CommandManager.register(Strings.CMD_FILE_OPEN,          Commands.FILE_OPEN, handleFileOpen);
        CommandManager.register(Strings.CMD_ADD_TO_WORKING_SET, Commands.FILE_ADD_TO_WORKING_SET, handleFileAddToWorkingSet);
        // TODO: (issue #274) For now, hook up File > New to the "new in project" handler. Eventually
        // File > New should open a new blank tab, and handleFileNewInProject should
        // be called from a "+" button in the project
        CommandManager.register(Strings.CMD_FILE_NEW,           Commands.FILE_NEW, handleFileNewInProject);
        CommandManager.register(Strings.CMD_FILE_NEW_FOLDER,    Commands.FILE_NEW_FOLDER, handleNewFolderInProject);
        CommandManager.register(Strings.CMD_FILE_SAVE,          Commands.FILE_SAVE, handleFileSave);
        CommandManager.register(Strings.CMD_FILE_SAVE_ALL,      Commands.FILE_SAVE_ALL, handleFileSaveAll);
        CommandManager.register(Strings.CMD_FILE_RENAME,        Commands.FILE_RENAME, handleFileRename);

        CommandManager.register(Strings.CMD_FILE_CLOSE,         Commands.FILE_CLOSE, handleFileClose);
        CommandManager.register(Strings.CMD_FILE_CLOSE_ALL,     Commands.FILE_CLOSE_ALL, handleFileCloseAll);
        CommandManager.register(Strings.CMD_CLOSE_WINDOW,       Commands.FILE_CLOSE_WINDOW, handleFileCloseWindow);
        CommandManager.register(Strings.CMD_QUIT,               Commands.FILE_QUIT, handleFileQuit);
        CommandManager.register(Strings.CMD_REFRESH_WINDOW,     Commands.DEBUG_REFRESH_WINDOW, handleFileReload);
        CommandManager.register(Strings.CMD_ABORT_QUIT,         Commands.APP_ABORT_QUIT, _handleAbortQuit);

        CommandManager.register(Strings.CMD_NEXT_DOC,           Commands.NAVIGATE_NEXT_DOC, handleGoNextDoc);
        CommandManager.register(Strings.CMD_PREV_DOC,           Commands.NAVIGATE_PREV_DOC, handleGoPrevDoc);
        CommandManager.register(Strings.CMD_SHOW_IN_TREE,       Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInTree);

        // Listen for changes that require updating the editor titlebar
        $(DocumentManager).on("dirtyFlagChange", handleDirtyChange);
        $(DocumentManager).on("currentDocumentChange fileNameChange", updateDocumentTitle);
    }

    // Define public API
    exports.init = init;
});


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Draggable - Events</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Drag me to trigger the chain of events.

"start" invoked 0x

"drag" invoked 0x

"stop" invoked 0x

Layer functionality onto the draggable using the start, drag, and stop events.  Start is fired at the start of the drag; drag during the drag; and stop when dragging stops.


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, $, brackets, waitsForDone */

define(function (require, exports, module) {
    "use strict";

    // Modules from the SpecRunner window
    var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
        Editor          = brackets.getModule("editor/Editor").Editor,
        CodeHintManager = brackets.getModule("editor/CodeHintManager"),
        HTMLCodeHints   = require("main");

    describe("HTML Attribute Hinting", function () {

        var defaultContent = "<!doctype html>\n" +
                             "\n" +
                             "\n" +
                             "\n" +
                             "
Heading
\n" +       // tag without whitespace
                             "
Subheading
\n" + // tag with whitespace
                             "

\n" +                         // tag without attributes
                             "

\n" +  // tag with two attributes
                             "  <div \n" +                           // incomplete tag
                             "\n" +
                             "\n";

        var testWindow;
        var testDocument, testEditor;

        beforeEach(function () {
            // create dummy Document for the Editor
            testDocument = SpecRunnerUtils.createMockDocument(defaultContent);

            // create Editor instance (containing a CodeMirror instance)
            $("body").append("
");
            testEditor = new Editor(testDocument, true, "htmlmixed", $("#editor").get(0), {});
        });

        afterEach(function () {
            testEditor.destroy();
            testEditor = null;
            $("#editor").remove();
            testDocument = null;
        });

        // Ask provider for hints at current cursor position; expect it to return some
        function expectHints(provider) {
            var query = provider.getQueryInfo(testEditor, testEditor.getCursorPos());
            expect(query).toBeTruthy();
            expect(query.queryStr).not.toBeNull();

            var hintList = provider.search(query);
            expect(hintList).toBeTruthy();

            return hintList;
        }

        // Ask provider for hints at current cursor position; expect it NOT to return any
        function expectNoHints(provider) {
            var query = provider.getQueryInfo(testEditor, testEditor.getCursorPos());
            expect(query).toBeTruthy();
            expect(query.queryStr).toBeNull();
        }

        // Expect hintList to contain tag names, starting with given value (if unspecified, expects the default unfilered list)
        function verifyTagHints(hintList, expectedFirstHint) {
            expect(hintList.indexOf("id")).toBe(-1);   // make sure attribute names aren't sneaking in there

            expectedFirstHint = expectedFirstHint || "a";  // assume unfiltered lists always start with "a"
            expect(hintList[0]).toBe(expectedFirstHint);
        }

        // Expect hintList to contain attribute names, starting with given value (if unspecified, expects the default unfilered list)
        function verifyAttrHints(hintList, expectedFirstHint) {
            expect(hintList.indexOf("div")).toBe(-1);   // make sure tag names aren't sneaking in there

            expectedFirstHint = expectedFirstHint || "accesskey";  // assume unfiltered lists always start with "accesskey"
            expect(hintList[0]).toBe(expectedFirstHint);
        }

        describe("Tag hint provider", function () {

            it("should not hint within \n" +
                                     "\n" +
                                     "\n" +
                                     "\n" +
                                     "\n");

                testEditor.setCursorPos({ line: 3, ch: 9 });        // cursor after the > in "


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Resizable - Synchronous resize</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Resize

will also resize

Resize multiple elements simultaneously by clicking and dragging the sides of one.  Pass a shared selector into the alsoResize option.


*          actually close any documents. This is useful when chaining close-all together with

*          other user prompts that may be cancelable.

* @return {$.Promise} a promise that is resolved when all files are closed

var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);

keep keep keep keep keep replace keep keep keep keep keep

promptOnly = commandData && commandData.promptOnly;


/*
 * dialog_options.js
 */
(function($) {

module("dialog: options");

test("autoOpen", function() {
	expect(2);

	el = $('

').dialog({ autoOpen: false });
		isNotOpen('.dialog({ autoOpen: false })');
	el.remove();

	el = $('

').dialog({ autoOpen: true });
		isOpen('.dialog({ autoOpen: true })');
	el.remove();
});

test("buttons", function() {
	expect(21);

	var btn, i, newButtons,
		buttons = {
		"Ok": function(ev, ui) {
			ok(true, "button click fires callback");
			equal(this, el[0], "context of callback");
			equal(ev.target, btn[0], "event target");
		},
		"Cancel": function(ev, ui) {
			ok(true, "button click fires callback");
			equal(this, el[0], "context of callback");
			equal(ev.target, btn[1], "event target");
		}
	};

	el = $('

').dialog({ buttons: buttons });
	btn = $("button", dlg());
	equal(btn.length, 2, "number of buttons");

	i = 0;
	$.each(buttons, function(key, val) {
		equal(btn.eq(i).text(), key, "text of button " + (i+1));
		i++;
	});

	ok(btn.parent().hasClass('ui-dialog-buttonset'), "buttons in container");
	ok(el.parent().hasClass('ui-dialog-buttons'), "dialog wrapper adds class about having buttons");

	btn.trigger("click");

	newButtons = {
		"Close": function(ev, ui) {
			ok(true, "button click fires callback");
			equal(this, el[0], "context of callback");
			equal(ev.target, btn[0], "event target");
		}
	};

	deepEqual(el.dialog("option", "buttons"), buttons, '.dialog("option", "buttons") getter');
	el.dialog("option", "buttons", newButtons);
	deepEqual(el.dialog("option", "buttons"), newButtons, '.dialog("option", "buttons", ...) setter');

	btn = $("button", dlg());
	equal(btn.length, 1, "number of buttons after setter");
	btn.trigger('click');

	i = 0;
	$.each(newButtons, function(key, val) {
		equal(btn.eq(i).text(), key, "text of button " + (i+1));
		i += 1;
	});

	el.dialog("option", "buttons", null);
	btn = $("button", dlg());
	equal(btn.length, 0, "all buttons have been removed");
	equal(el.find(".ui-dialog-buttonset").length, 0, "buttonset has been removed");
	equal(el.parent().hasClass('ui-dialog-buttons'), false, "dialog wrapper removes class about having buttons");

	el.remove();
});

test("buttons - advanced", function() {
	expect(5);

	el = $("

").dialog({
		buttons: [
			{
				text: "a button",
				"class": "additional-class",
				id: "my-button-id",
				click: function() {
					equal(this, el[0], "correct context");
				}
			}
		]
	});
	var buttons = dlg().find("button");
	equal(buttons.length, 1, "correct number of buttons");
	equal(buttons.attr("id"), "my-button-id", "correct id");
	equal(buttons.text(), "a button", "correct label");
	ok(buttons.hasClass("additional-class"), "additional classes added");
	buttons.click();

	el.remove();
});

test("closeOnEscape", function() {
	el = $('

').dialog({ closeOnEscape: false });
	ok(true, 'closeOnEscape: false');
	ok(dlg().is(':visible') && !dlg().is(':hidden'), 'dialog is open before ESC');
	el.simulate('keydown', { keyCode: $.ui.keyCode.ESCAPE })
		.simulate('keypress', { keyCode: $.ui.keyCode.ESCAPE })
		.simulate('keyup', { keyCode: $.ui.keyCode.ESCAPE });
	ok(dlg().is(':visible') && !dlg().is(':hidden'), 'dialog is open after ESC');

	el.remove();

	el = $('

').dialog({ closeOnEscape: true });
	ok(true, 'closeOnEscape: true');
	ok(dlg().is(':visible') && !dlg().is(':hidden'), 'dialog is open before ESC');
	el.simulate('keydown', { keyCode: $.ui.keyCode.ESCAPE })
		.simulate('keypress', { keyCode: $.ui.keyCode.ESCAPE })
		.simulate('keyup', { keyCode: $.ui.keyCode.ESCAPE });
	ok(dlg().is(':hidden') && !dlg().is(':visible'), 'dialog is closed after ESC');
});

test("closeText", function() {
	expect(3);

	el = $('

').dialog();
		equal(dlg().find('.ui-dialog-titlebar-close span').text(), 'close',
			'default close text');
	el.remove();

	el = $('

').dialog({ closeText: "foo" });
		equal(dlg().find('.ui-dialog-titlebar-close span').text(), 'foo',
			'closeText on init');
	el.remove();

	el = $('

').dialog().dialog('option', 'closeText', 'bar');
		equal(dlg().find('.ui-dialog-titlebar-close span').text(), 'bar',
			'closeText via option method');
	el.remove();
});

test("dialogClass", function() {
	expect(4);

	el = $('

').dialog();
		equal(dlg().is(".foo"), false, 'dialogClass not specified. foo class added');
	el.remove();

	el = $('

').dialog({ dialogClass: "foo" });
		equal(dlg().is(".foo"), true, 'dialogClass in init. foo class added');
	el.remove();

	el = $('

').dialog({ dialogClass: "foo bar" });
		equal(dlg().is(".foo"), true, 'dialogClass in init, two classes. foo class added');
		equal(dlg().is(".bar"), true, 'dialogClass in init, two classes. bar class added');
	el.remove();
});

test("draggable", function() {
	expect(4);

	el = $('

').dialog({ draggable: false });
		shouldnotmove();
		el.dialog('option', 'draggable', true);
		shouldmove();
	el.remove();

	el = $('

').dialog({ draggable: true });
		shouldmove();
		el.dialog('option', 'draggable', false);
		shouldnotmove();
	el.remove();
});

test("height", function() {
	expect(4);

	el = $('

').dialog();
		equal(dlg().outerHeight(), 150, "default height");
	el.remove();

	el = $('

').dialog({ height: 237 });
		equal(dlg().outerHeight(), 237, "explicit height");
	el.remove();

	el = $('

').dialog();
		el.dialog('option', 'height', 238);
		equal(dlg().outerHeight(), 238, "explicit height set after init");
	el.remove();

	el = $('

').css("padding", "20px")
		.dialog({ height: 240 });
		equal(dlg().outerHeight(), 240, "explicit height with padding");
	el.remove();
});

test("maxHeight", function() {
	expect(3);

	el = $('

').dialog({ maxHeight: 200 });
		drag('.ui-resizable-s', 1000, 1000);
		equal(heightAfter, 200, "maxHeight");
	el.remove();

	el = $('

').dialog({ maxHeight: 200 });
		drag('.ui-resizable-n', -1000, -1000);
		equal(heightAfter, 200, "maxHeight");
	el.remove();

	el = $('

').dialog({ maxHeight: 200 }).dialog('option', 'maxHeight', 300);
		drag('.ui-resizable-s', 1000, 1000);
		equal(heightAfter, 300, "maxHeight");
	el.remove();
});

test("maxWidth", function() {
	expect(3);

	el = $('

').dialog({ maxWidth: 200 });
		drag('.ui-resizable-e', 1000, 1000);
		equal(widthAfter, 200, "maxWidth");
	el.remove();

	el = $('

').dialog({ maxWidth: 200 });
		drag('.ui-resizable-w', -1000, -1000);
		equal(widthAfter, 200, "maxWidth");
	el.remove();

	el = $('

').dialog({ maxWidth: 200 }).dialog('option', 'maxWidth', 300);
		drag('.ui-resizable-w', -1000, -1000);
		equal(widthAfter, 300, "maxWidth");
	el.remove();
});

test("minHeight", function() {
	expect(3);

	el = $('

').dialog({ minHeight: 10 });
		drag('.ui-resizable-s', -1000, -1000);
		equal(heightAfter, 10, "minHeight");
	el.remove();

	el = $('

').dialog({ minHeight: 10 });
		drag('.ui-resizable-n', 1000, 1000);
		equal(heightAfter, 10, "minHeight");
	el.remove();

	el = $('

').dialog({ minHeight: 10 }).dialog('option', 'minHeight', 30);
		drag('.ui-resizable-n', 1000, 1000);
		equal(heightAfter, 30, "minHeight");
	el.remove();
});

test("minWidth", function() {
	expect(3);

	el = $('

').dialog({ minWidth: 10 });
		drag('.ui-resizable-e', -1000, -1000);
		equal(widthAfter, 10, "minWidth");
	el.remove();

	el = $('

').dialog({ minWidth: 10 });
		drag('.ui-resizable-w', 1000, 1000);
		equal(widthAfter, 10, "minWidth");
	el.remove();

	el = $('

').dialog({ minWidth: 30 }).dialog('option', 'minWidth', 30);
		drag('.ui-resizable-w', 1000, 1000);
		equal(widthAfter, 30, "minWidth");
	el.remove();
});

test("position, default center on window", function() {
	var el = $('

').dialog(),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, Math.round($(window).width() / 2 - dialog.outerWidth() / 2) + $(window).scrollLeft());
	deepEqual(offset.top, Math.round($(window).height() / 2 - dialog.outerHeight() / 2) + $(window).scrollTop());
	el.remove();
});

test("position, top on window", function() {
	var el = $('

').dialog({ position: "top" }),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, Math.round($(window).width() / 2 - dialog.outerWidth() / 2) + $(window).scrollLeft());
	deepEqual(offset.top, $(window).scrollTop());
	el.remove();
});

test("position, left on window", function() {
	var el = $('

').dialog({ position: "left" }),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, 0);
	deepEqual(offset.top, Math.round($(window).height() / 2 - dialog.outerHeight() / 2) + $(window).scrollTop());
	el.remove();
});

test("position, right bottom on window", function() {
	var el = $('

').dialog({ position: "right bottom" }),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, $(window).width() - dialog.outerWidth() + $(window).scrollLeft());
	deepEqual(offset.top, $(window).height() - dialog.outerHeight() + $(window).scrollTop());
	el.remove();
});

test("position, right bottom on window w/array", function() {
	var el = $('

').dialog({ position: ["right", "bottom"] }),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, $(window).width() - dialog.outerWidth() + $(window).scrollLeft());
	deepEqual(offset.top, $(window).height() - dialog.outerHeight() + $(window).scrollTop());
	el.remove();
});

test("position, offset from top left w/array", function() {
	var el = $('

').dialog({ position: [10, 10] }),
		dialog = el.dialog('widget'),
		offset = dialog.offset();
	deepEqual(offset.left, 10 + $(window).scrollLeft());
	deepEqual(offset.top, 10 + $(window).scrollTop());
	el.remove();
});

test("position, right bottom at right bottom via ui.position args", function() {
	var el = $('

').dialog({
			position: {
				my: "right bottom",
				at: "right bottom"
			}
		}),
		dialog = el.dialog('widget'),
		offset = dialog.offset();

	deepEqual(offset.left, $(window).width() - dialog.outerWidth() + $(window).scrollLeft());
	deepEqual(offset.top, $(window).height() - dialog.outerHeight() + $(window).scrollTop());
	el.remove();
});

test("position, at another element", function() {
	var parent = $('

').css({
			position: 'absolute',
			top: 400,
			left: 600,
			height: 10,
			width: 10
		}).appendTo('body'),

		el = $('

').dialog({
			position: {
				my: "left top",
				at: "left top",
				of: parent
			}
		}),

		dialog = el.dialog('widget'),
		offset = dialog.offset();

	deepEqual(offset.left, 600);
	deepEqual(offset.top, 400);

	el.dialog('option', 'position', {
			my: "left top",
			at: "right bottom",
			of: parent
	});

	offset = dialog.offset();

	deepEqual(offset.left, 610);
	deepEqual(offset.top, 410);

	el.remove();
	parent.remove();
});

test("resizable", function() {
	expect(4);

	el = $('

').dialog();
		shouldresize("[default]");
		el.dialog('option', 'resizable', false);
		shouldnotresize('disabled after init');
	el.remove();

	el = $('

').dialog({ resizable: false });
		shouldnotresize("disabled in init options");
		el.dialog('option', 'resizable', true);
		shouldresize('enabled after init');
	el.remove();
});

test("title", function() {
	expect(9);

	function titleText() {
		return dlg().find(".ui-dialog-title").html();
	}

	el = $('

').dialog();
		// some browsers return a non-breaking space and some return " "
		// so we get the text to normalize to the actual non-breaking space
		equal(dlg().find(".ui-dialog-title").text(), " ", "[default]");
		equal(el.dialog("option", "title"), "", "option not changed");
	el.remove();

	el = $('
').dialog();
		equal(titleText(), "foo", "title in element attribute");
		equal(el.dialog("option", "title"), "foo", "option updated from attribute");
	el.remove();

	el = $('

').dialog({ title: 'foo' });
		equal(titleText(), "foo", "title in init options");
		equal(el.dialog("option", "title"), "foo", "opiton set from options hash");
	el.remove();

	el = $('
').dialog({ title: 'bar' });
		equal(titleText(), "bar", "title in init options should override title in element attribute");
		equal(el.dialog("option", "title"), "bar", "opiton set from options hash");
	el.remove();

	el = $('

').dialog().dialog('option', 'title', 'foo');
		equal(titleText(), 'foo', 'title after init');
	el.remove();
});

test("width", function() {
	expect(3);

	el = $('

').dialog();
		equal(dlg().width(), 300, "default width");
	el.remove();

	el = $('

').dialog({width: 437 });
		equal(dlg().width(), 437, "explicit width");
		el.dialog('option', 'width', 438);
		equal(dlg().width(), 438, 'explicit width after init');
	el.remove();
});

})(jQuery);


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window */

/**
 * Utilities for creating and managing standard modal dialogs.
 */
define(function (require, exports, module) {
    "use strict";

    require("utils/Global");

    var KeyBindingManager = require("command/KeyBindingManager"),
        KeyEvent          = require("utils/KeyEvent"),
        NativeApp         = require("utils/NativeApp");

    var DIALOG_BTN_CANCEL = "cancel",
        DIALOG_BTN_OK = "ok",
        DIALOG_BTN_DONTSAVE = "dontsave",
        DIALOG_CANCELED = "_canceled",
        DIALOG_BTN_DOWNLOAD = "download";

    // TODO: (issue #258) In future, we should templatize the HTML for the dialogs rather than having
    // it live directly in the HTML.
    var DIALOG_ID_ERROR = "error-dialog",
        DIALOG_ID_INFO = "error-dialog", // uses the same template for now--could be different in future
        DIALOG_ID_SAVE_CLOSE = "save-close-dialog",
        DIALOG_ID_EXT_CHANGED = "ext-changed-dialog",
        DIALOG_ID_EXT_DELETED = "ext-deleted-dialog",
        DIALOG_ID_LIVE_DEVELOPMENT = "live-development-error-dialog",
        DIALOG_ID_ABOUT = "about-dialog",
        DIALOG_ID_UPDATE = "update-dialog",
        DIALOG_ID_PROJECT_SETTINGS = "project-settings-dialog";

    function _dismissDialog(dlg, buttonId) {
        dlg.data("buttonId", buttonId);
        $(".clickable-link", dlg).off("click");
        dlg.modal(true).hide();
    }

    function _hasButton(dlg, buttonId) {
        return dlg.find("[data-button-id='" + buttonId + "']");
    }

    var _handleKeyDown = function (e) {
        var primaryBtn = this.find(".primary"),
            buttonId = null,
            which = String.fromCharCode(e.which);

        // There might be a textfield in the dialog's UI; don't want to mistake normal typing for dialog dismissal
        var inFormField = ($(e.target).filter(":input").length > 0),
            inTextArea = (e.target.tagName === "TEXTAREA");

        if (e.which === KeyEvent.DOM_VK_RETURN && !inTextArea) {  // enter key in single-line text input still dismisses
            // Click primary button
            if (primaryBtn) {
                buttonId = primaryBtn.attr("data-button-id");
            }
        } else if (e.which === KeyEvent.DOM_VK_SPACE) {
            // Space bar on focused button
            this.find(".dialog-button:focus").click();
        } else if (brackets.platform === "mac") {
            // CMD+D Don't Save
            if (e.metaKey && (which === "D")) {
                if (_hasButton(this, DIALOG_BTN_DONTSAVE)) {
                    buttonId = DIALOG_BTN_DONTSAVE;
                }
            // FIXME (issue #418) CMD+. Cancel swallowed by native shell
            } else if (e.metaKey && (e.which === KeyEvent.DOM_VK_PERIOD)) {
                buttonId = DIALOG_BTN_CANCEL;
            }
        } else { // if (brackets.platform === "win") {
            // 'N' Don't Save
            if (which === "N" && !inFormField) {
                if (_hasButton(this, DIALOG_BTN_DONTSAVE)) {
                    buttonId = DIALOG_BTN_DONTSAVE;
                }
            }
        }

        if (buttonId) {
            _dismissDialog(this, buttonId);
        } else if (!($.contains(this.get(0), e.target)) || !inFormField) {
            // Stop the event if the target is not inside the dialog
            // or if the target is not a form element.
            // TODO (issue #414): more robust handling of dialog scoped
            //                    vs. global key bindings
            e.stopPropagation();
            e.preventDefault();
        }
    };

    /**
     * General purpose modal dialog. Assumes that:
     * -- the root tag of the dialog is marked with a unique class name (passed as dlgClass), as well as the
     *    classes "template modal hide".
     * -- the HTML for the dialog contains elements with "title" and "message" classes, as well as a number
     *    of elements with "dialog-button" class, each of which has a "data-button-id".
     *
     * @param {string} dlgClass The class of the dialog node in the HTML.
     * @param {string=} title The title of the error dialog. Can contain HTML markup. If unspecified, title in
     *      the HTML template is used unchanged.
     * @param {string=} message The message to display in the error dialog. Can contain HTML markup. If
     *      unspecified, body in the HTML template is used unchanged.
     * @return {$.Promise} a promise that will be resolved with the ID of the clicked button when the dialog
     *     is dismissed. Never rejected.
     */
    function showModalDialog(dlgClass, title, message) {
        var result = $.Deferred(),
            promise = result.promise();

        // We clone the HTML rather than using it directly so that if two dialogs of the same
        // type happen to show up, they can appear at the same time. (This is an edge case that
        // shouldn't happen often, but we can't prevent it from happening since everything is
        // asynchronous.)
        var $dlg = $("." + dlgClass + ".template")
            .clone()
            .removeClass("template")
            .addClass("instance")
            .appendTo(window.document.body);

        if ($dlg.length === 0) {
            throw new Error("Dialog id " + dlgClass + " does not exist");
        }

        // Save the dialog promise for unit tests
        $dlg.data("promise", promise);

        // Set title and message
        if (title) {
            $(".dialog-title", $dlg).html(title);
        }
        if (message) {
            $(".dialog-message", $dlg).html(message);
        }

        $(".clickable-link", $dlg).on("click", function _handleLink(e) {
            // Links use data-href (not href) attribute so Brackets itself doesn't redirect
            if (e.target.dataset && e.target.dataset.href) {
                NativeApp.openURLInDefaultBrowser(e.target.dataset.href);
            }
        });

        var handleKeyDown = _handleKeyDown.bind($dlg);

        // Pipe dialog-closing notification back to client code
        $dlg.one("hidden", function () {
            var buttonId = $dlg.data("buttonId");
            if (!buttonId) {    // buttonId will be undefined if closed via Bootstrap's "x" button
                buttonId = DIALOG_BTN_CANCEL;
            }

            // Let call stack return before notifying that dialog has closed; this avoids issue #191
            // if the handler we're triggering might show another dialog (as long as there's no
            // fade-out animation)
            window.setTimeout(function () {
                result.resolve(buttonId);
            }, 0);

            // Remove the dialog instance from the DOM.
            $dlg.remove();

            // Remove keydown event handler
            window.document.body.removeEventListener("keydown", handleKeyDown, true);
            KeyBindingManager.setEnabled(true);
        }).one("shown", function () {
            // Set focus to the default button
            var primaryBtn = $dlg.find(".primary");

            if (primaryBtn) {
                primaryBtn.focus();
            }

            // Listen for dialog keyboard shortcuts
            window.document.body.addEventListener("keydown", handleKeyDown, true);
            KeyBindingManager.setEnabled(false);
        });

        // Click handler for buttons
        $dlg.one("click", ".dialog-button", function (e) {
            _dismissDialog($dlg, $(this).attr("data-button-id"));
        });

        // Run the dialog
        $dlg.modal({
            backdrop: "static",
            show: true,
            keyboard: true
        });

        return promise;
    }

    /**
     * Immediately closes any dialog instances with the given class. The dialog callback for each instance will
     * be called with the special buttonId DIALOG_CANCELED (note: callback is run asynchronously).
     */
    function cancelModalDialogIfOpen(dlgClass) {
        $("." + dlgClass + ".instance").each(function (index, dlg) {
            if ($(dlg).is(":visible")) {   // Bootstrap breaks if try to hide dialog that's already hidden
                _dismissDialog($(dlg), DIALOG_CANCELED);
            }
        });
    }

    exports.DIALOG_BTN_CANCEL   = DIALOG_BTN_CANCEL;
    exports.DIALOG_BTN_OK       = DIALOG_BTN_OK;
    exports.DIALOG_BTN_DONTSAVE = DIALOG_BTN_DONTSAVE;
    exports.DIALOG_CANCELED     = DIALOG_CANCELED;
    exports.DIALOG_BTN_DOWNLOAD = DIALOG_BTN_DOWNLOAD;

    exports.DIALOG_ID_ERROR             = DIALOG_ID_ERROR;
    exports.DIALOG_ID_INFO              = DIALOG_ID_INFO;
    exports.DIALOG_ID_SAVE_CLOSE        = DIALOG_ID_SAVE_CLOSE;
    exports.DIALOG_ID_EXT_CHANGED       = DIALOG_ID_EXT_CHANGED;
    exports.DIALOG_ID_EXT_DELETED       = DIALOG_ID_EXT_DELETED;
    exports.DIALOG_ID_LIVE_DEVELOPMENT  = DIALOG_ID_LIVE_DEVELOPMENT;
    exports.DIALOG_ID_ABOUT             = DIALOG_ID_ABOUT;
    exports.DIALOG_ID_UPDATE            = DIALOG_ID_UPDATE;
    exports.DIALOG_ID_PROJECT_SETTINGS  = DIALOG_ID_PROJECT_SETTINGS;

    exports.showModalDialog             = showModalDialog;
    exports.cancelModalDialogIfOpen     = cancelModalDialogIfOpen;
});


function Song() {
}

Song.prototype.persistFavoriteStatus = function(value) {
  // something complicated
  throw new Error("not yet implemented");
};


// Only one unsaved file: show the usual single-file-close confirmation UI

var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: commandData.promptOnly };

// still need to close any other, non-unsaved documents

keep keep keep keep replace keep keep keep

var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: promptOnly };


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Resizable - Constrain resize area</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Containment

Resizable

Define the boundaries of the resizable area. Use the containment option to specify a parent DOM element or a jQuery selector, like 'document.'


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Droppable - Prevent propagation</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Drag me to my target

Outer droppable

Inner droppable (not greedy)

Outer droppable

Inner droppable (greedy)

When working with nested droppables — for example, you may have an editable directory structure displayed as a tree, with folder and document nodes — the greedy option set to true prevents event propagation when a draggable is dropped on a child node (droppable).


// If all the unsaved-changes confirmations pan out above, then go ahead & close all editors

// NOTE: this still happens before any done() handlers added by our caller, because jQ

// guarantees that handlers run in the order they are added.

if (!commandData || !commandData.promptOnly) {

keep keep keep keep keep replace keep keep keep


## Localization in Brackets
Brackets has basic support for displaying user visible text in other languages. The document describes how localized text can be added to core Brackets code and Brackets Plugins

Javascript and HTML that displays user visible strings in Brackets should not use static English strings directly in code. Instead,  strings should be stored in a the file brackets \src\nls\root\strings.js. Code that wishes to display strings should import the String module from src\string.js. This module dynamically loads a strings.js file in the nls folder for the current local. JavaScript can then directly reference properties of the String module to access strings, and HTML can use the String module in combination with Mustache.js templating to display localized strings. Brackets uses the i18n RequireJS plugin for to load localized strings. The plugin loads the strings.js file according to the user’s locale and also provides a fallback to English for missing translations.

## Adding new localized strings to Brackets
Add new English strings to the file brackets\src\nls\root\strings.js. Translations in other languages should be added to the strings.js file in each of the locale folders inside the “nls” folder. For example, modify brackets\src\nls\fr\strings.js for French

## Using Localized strings in Brackets modules

## Strings JavaScript
1. Load the Strings module in the file where the string is to be used
`Strings = require("strings");`

2. Reference a string by selecting a property from the String module. For example, `Strings.NOT_FOUND_ERR` yields the string “The file could not be found." for an English locale.

## Strings HTML
Localizing HTML in Brackets requires use of the templating framework mustache.js since HTML cannot directly reference properties in strings.js. Strings for both JavaScript and HTML are stored \src\nls\root\strings.js, but in HTML they are referenced using the template syntax `{{stringKeyName}}`.

For example,

`<h1 class="dialog-title">{{SAVE_CHANGES}}</h1>`

yields

`<h1 class="dialog-title">Save Changes</h1>`

Mustache recognizes this syntax and can substite the appropriate string. Mustache is run when Brackets launches during module load. Code in brackets.js loads the appropriate strings.js file using i18n, loads html content from a file as a text, runs Mustache on the text using strings.js for the data lookup, and then inserts the result into the DOM. Core brackets code that depends on the DOM should listen for the “htmlContentLoadComplete” before inspecting or manipulating the dome

## Localization Guidelines
Multiple strings keys in strings.js should not be concatenated together since word order often varies between languages

## Limitations
Brackets does not support localizing keyboard shortcuts yet

## Localization Support for Brackets Plugins

Localization in plugins works very similarly to how localization works for core Brackets module. The i18n RequireJS plugin is used to dynamically load a string.js file for the appropriate locale. JavaString can then reference strings via property name from the loaded string module and mustache can be used on on html fragments to insert localized text.

For an example of a simple localized plugin see: brackets\src\extensions\disabled\LocalizationExample

Move this plugin to the extensions\user\ folder to run the plugin. It will add a "My New Command" menu item to the end of the Edit menu. This command shows an alert with localized text and then a modal dialog with localized HTML content.

#### Below is the folder struture and comments on the role of each file

* main.js – loads the Strings module for the plugin and uses mustache to localize html content

* strings.js – uses i18n to load a strings.js file in the nls folder

* htmlContent
    * htmlfragment.html – html template to be localized by mustache
    * nls
        * strings.js – configures i18n by specifying the root folder  and listing the locales supported by the plugin
        * root
		    * strings.js – contains the English strings
	    * fr
		    * strings.js – contains the French strings
	    * etc. for each locale

#### Strings for plugins vs Brackets
Note that there is a distinction between loading strings for a plugin vs. strings in the Brackets core. To access strings local to your plugin use `var strings = require("strings")`. To load the core Brackets strings use `var bracketsStrings = brackets.getModule("strings")`


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Autocomplete - XML data parsed once</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

	London matches:
	<input id="birds" />

	Result:

This demo shows how to retrieve some XML data, parse it using jQuery's methods, then provide it to the autocomplete as the datasource.

This should also serve as a reference on how to parse a remote XML datasource - the parsing would just happen for each request within the source-callback.


* {
    color: "red";
}

.firstGrade {
    color: "white";
}

p {
    color: "blue";
}

#brack3ts {
    color: "yellow";
}