adobe/brackets

View on GitHub
src/view/ViewCommandHandlers.js

Summary

Maintainability
C
1 day
Test Coverage
/*
 * Copyright (c) 2012 - present 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.
 *
 */

/*global less */

/**
 * The ViewCommandHandlers object dispatches the following event(s):
 *    - fontSizeChange -- Triggered when the font size is changed via the
 *      Increase Font Size, Decrease Font Size, or Restore Font Size commands.
 *      The 2nd arg to the listener is the amount of the change. The 3rd arg
 *      is a string containing the new font size after applying the change.
 */
define(function (require, exports, module) {
    "use strict";

    var Commands            = require("command/Commands"),
        EventDispatcher     = require("utils/EventDispatcher"),
        CommandManager      = require("command/CommandManager"),
        Strings             = require("strings"),
        StringUtils         = require("utils/StringUtils"),
        EditorManager       = require("editor/EditorManager"),
        PreferencesManager  = require("preferences/PreferencesManager"),
        DocumentManager     = require("document/DocumentManager"),
        ThemeSettings       = require("view/ThemeSettings"),
        MainViewManager     = require("view/MainViewManager"),
        AppInit             = require("utils/AppInit"),
        _                   = require("thirdparty/lodash"),
        FontRuleTemplate    = require("text!view/fontrules/font-based-rules.less");

    var prefs = PreferencesManager.getExtensionPrefs("fonts");
    
    
    /**
     * Font sizes should be validated by this regexp
     */
    var validFontSizeRegExpStr = "^([0-9]+)?(\\.)?([0-9]+)(px|em)$";
    // Need RegExp as a string to be exported for use with HTML5 pattern attribute

    /**
     * @private
     * The currently present font size. Used to detect no-op changes.
     * @type {string}
     */
    var currFontSize;

    /**
     * @private
     * The currently present font family. Used to detect no-op changes.
     * @type {string}
     */
    var currFontFamily;

    /**
     * @const
     * @type {string}
     */
    var DYNAMIC_FONT_STYLE_ID = "codemirror-dynamic-fonts";

    /**
     * @const
     * @type {string}
     */
    var DYNAMIC_FONT_FAMILY_ID = "codemirror-dynamic-font-family";

    /**
     * @const
     * @private
     * The smallest font size in pixels
     * @type {number}
     */
    var MIN_FONT_SIZE = 1;

    /**
     * @const
     * @private
     * The largest font size in pixels
     * @type {number}
     */
    var MAX_FONT_SIZE = 72;

    /**
     * @const
     * @private
     * The default font size used only to convert the old fontSizeAdjustment view state to the new fontSize
     * @type {number}
     */
    var DEFAULT_FONT_SIZE = 12;

    /**
     * @const
     * @private
     * The default font family
     * @type {string}
     */
    var DEFAULT_FONT_FAMILY = "'SourceCodePro-Medium', MS ゴシック, 'MS Gothic', monospace";
    
    /**
     * @private
     * Removes style property from the DOM
     * @param {string} propertyID is the id of the property to be removed
     */
    function _removeDynamicProperty(propertyID) {
        $("#" + propertyID).remove();
    }
    
    /**
     * @private
     * Add the style property to the DOM
     * @param {string} propertyID Is the property ID to be added
     * @param {object} ruleCfg Is the CSS Rule configuration object
     * @param {string} ruleCfg.propName Is the name of the style property
     * @param {string} ruleCfg.propValue Is the value of the style property
     * @param {boolean} ruleCfg.priorityFlag Is a flag to make the style property !important
     * @param {string} ruleCfg.ruleName Optional Selctor name to be used for the rule
     * @param {string} ruleCfg.ruleText Optional selector definition text
     */
    function _addDynamicProperty(propertyID, ruleCfg) {
        var $style   = $("<style type='text/css'></style>").attr("id", propertyID);
        if (ruleCfg.ruleText) {
            $style.html(ruleCfg.ruleText);
        } else {
            var cssRule = ruleCfg.ruleName || ".CodeMirror";
            var styleStr = ruleCfg.ruleText || StringUtils.format("{0}: {1} {2}", ruleCfg.propName, ruleCfg.propValue, ruleCfg.priorityFlag ? "!important" : "");
            $style.html(cssRule + "{ " + styleStr + " }");
        }

        // Let's make sure we remove the already existing item from the DOM.
        _removeDynamicProperty(propertyID);
        $("head").append($style);
    }
    
    /**
     * @private
     * Removes the styles used to update the font size
     */
    function _removeDynamicFontSize() {
        _removeDynamicProperty(DYNAMIC_FONT_STYLE_ID);
    }

    /**
     * @private
     * Add the styles used to update the font size
     * @param {string} fontSize  A string with the font size and the size unit
     */
    function _addDynamicFontSize(fontSize) {
        var template = FontRuleTemplate.split("{font-size-param}").join(fontSize);
        less.render(template, null, function onParse(err, tree) {
            if (err) {
                console.error(err);
            } else {
                _addDynamicProperty(DYNAMIC_FONT_STYLE_ID, {
                    ruleText: tree.css
                });
            }
        });
    }

    /**
     * @private
     * Removes the styles used to update the font family
     */
    function _removeDynamicFontFamily() {
        _removeDynamicProperty(DYNAMIC_FONT_FAMILY_ID);
    }

    /**
     * @private
     * Add the styles used to update the font family
     * @param {string} fontFamily  A string with the font family
     */
    function _addDynamicFontFamily(fontFamily) {
        _addDynamicProperty(DYNAMIC_FONT_FAMILY_ID, {
            propName: "font-family",
            propValue: fontFamily
        });
    }

    /**
     * @private
     * Sets the font size and restores the scroll position as best as possible.
     * @param {!Editor} editor  Editor to update.
     * @param {string=} fontSize  A string with the font size and the size unit
     */
    function _updateScroll(editor, fontSize) {
        var oldWidth    = editor._codeMirror.defaultCharWidth(),
            oldFontSize = prefs.get("fontSize"),
            newFontSize = fontSize,
            delta       = 0,
            adjustment  = 0,
            scrollPos   = editor.getScrollPos(),
            line        = editor._codeMirror.lineAtHeight(scrollPos.y, "local");

        delta = /em$/.test(oldFontSize) ? 10 : 1;
        adjustment = parseInt((parseFloat(newFontSize) - parseFloat(oldFontSize)) * delta, 10);

        // Only adjust the scroll position if there was any adjustments to the font size.
        // Otherwise there will be unintended scrolling.
        //
        if (adjustment) {
            editor.refreshAll();
        }

        // Calculate the new scroll based on the old font sizes and scroll position
        var newWidth   = editor._codeMirror.defaultCharWidth(),
            deltaX     = scrollPos.x / oldWidth,
            scrollPosX = scrollPos.x + Math.round(deltaX * (newWidth  - oldWidth)),
            scrollPosY = editor._codeMirror.heightAtLine(line, "local");

        editor.setScrollPos(scrollPosX, scrollPosY);
    }

    /**
     * Font size setter to set the font size for the document editor
     * @param {string} fontSize The font size with size unit as 'px' or 'em'
     */
    function setFontSize(fontSize) {
        if (currFontSize === fontSize) {
            return;
        }

        _removeDynamicFontSize();
        if (fontSize) {
            _addDynamicFontSize(fontSize);
        }

        // Update scroll metrics in viewed editors
        _.forEach(MainViewManager.getPaneIdList(), function (paneId) {
            var currentPath = MainViewManager.getCurrentlyViewedPath(paneId),
                doc = currentPath && DocumentManager.getOpenDocumentForPath(currentPath);
            if (doc && doc._masterEditor) {
                _updateScroll(doc._masterEditor, fontSize);
            }
        });

        exports.trigger("fontSizeChange", fontSize, currFontSize);
        currFontSize = fontSize;
        prefs.set("fontSize", fontSize);
    }

    /**
     * Font size getter to get the current font size for the document editor
     * @return {string} Font size with size unit as 'px' or 'em'
     */
    function getFontSize() {
        return prefs.get("fontSize");
    }


    /**
     * Font family setter to set the font family for the document editor
     * @param {string} fontFamily The font family to be set.  It can be a string with multiple comma separated fonts
     */
    function setFontFamily(fontFamily) {
        var editor = EditorManager.getCurrentFullEditor();

        if (currFontFamily === fontFamily) {
            return;
        }

        _removeDynamicFontFamily();
        if (fontFamily) {
            _addDynamicFontFamily(fontFamily);
        }

        exports.trigger("fontFamilyChange", fontFamily, currFontFamily);
        currFontFamily = fontFamily;
        prefs.set("fontFamily", fontFamily);

        if (editor) {
            editor.refreshAll();
        }
    }


    /**
     * Font smoothing setter to set the anti-aliasing type for the code area on Mac.
     * @param {string} aaType The antialiasing type to be set. It can take either "subpixel-antialiased" or "antialiased"
     */
    function setMacFontSmoothingType(aaType) {
        var $editor_holder  = $("#editor-holder");

        // Add/Remove the class based on the preference. Also
        // default to subpixel AA in case of invalid entries.
        if (aaType === "antialiased") {
            $editor_holder.removeClass("subpixel-aa");
        } else {
            $editor_holder.addClass("subpixel-aa");
        }
    }

    /**
     * Font family getter to get the currently configured font family for the document editor
     * @return {string} The font family for the document editor
     */
    function getFontFamily() {
        return prefs.get("fontFamily");
    }


    /**
     * @private
     * Increases or decreases the editor's font size.
     * @param {number} adjustment  Negative number to make the font smaller; positive number to make it bigger
     * @return {boolean} true if adjustment occurred, false if it did not occur
     */
     function _adjustFontSize(adjustment) {
        var fsStyle    = prefs.get("fontSize");
        var validFontSizeRegExp = new RegExp(validFontSizeRegExpStr);
        
        // Make sure that the font size is expressed in terms we can
        // handle (px or em). If not, simply bail.
        
         if (fsStyle.search(validFontSizeRegExp) === -1) {
            return false;
        }

        // Guaranteed to work by validation above.
        var fsUnits = fsStyle.substring(fsStyle.length - 2, fsStyle.length),
            delta   = fsUnits === "px" ? 1 : 0.1,
            fsOld   = parseFloat(fsStyle.substring(0, fsStyle.length - 2)),
            fsNew   = fsOld + (delta * adjustment),
            fsStr   = fsNew + fsUnits;

        // Don't let the font size get too small or too large. The minimum font size is 1px or 0.1em
        // and the maximum font size is 72px or 7.2em depending on the unit used
        if (fsNew < MIN_FONT_SIZE * delta || fsNew > MAX_FONT_SIZE * delta) {
            return false;
        }

        setFontSize(fsStr);
        return true;
    }

    /** Increases the font size by 1 */
    function _handleIncreaseFontSize() {
        _adjustFontSize(1);
    }

    /** Decreases the font size by 1 */
    function _handleDecreaseFontSize() {
        _adjustFontSize(-1);
    }

    /** Restores the font size to the original size */
    function _handleRestoreFontSize() {
        setFontSize(DEFAULT_FONT_SIZE + "px");
    }

    /**
     * @private
     * Updates the user interface appropriately based on whether or not a document is
     * currently open in the editor.
     */
    function _updateUI() {
        if (DocumentManager.getCurrentDocument() !== null) {
            if (!CommandManager.get(Commands.VIEW_INCREASE_FONT_SIZE).getEnabled()) {
                // If one is disabled then they all are disabled, so enable them all
                CommandManager.get(Commands.VIEW_INCREASE_FONT_SIZE).setEnabled(true);
                CommandManager.get(Commands.VIEW_DECREASE_FONT_SIZE).setEnabled(true);
                CommandManager.get(Commands.VIEW_RESTORE_FONT_SIZE).setEnabled(true);
            }
        } else {
            // No current document so disable all of the Font Size commands
            CommandManager.get(Commands.VIEW_INCREASE_FONT_SIZE).setEnabled(false);
            CommandManager.get(Commands.VIEW_DECREASE_FONT_SIZE).setEnabled(false);
            CommandManager.get(Commands.VIEW_RESTORE_FONT_SIZE).setEnabled(false);
        }
    }

    /**
     * Initializes the different settings that need to loaded
     */
    function init() {
        currFontFamily = prefs.get("fontFamily");
        _addDynamicFontFamily(currFontFamily);
        currFontSize = prefs.get("fontSize");
        _addDynamicFontSize(currFontSize);
        _updateUI();
    }

    /**
     * Restores the font size using the saved style and migrates the old fontSizeAdjustment
     * view state to the new fontSize, when required
     */
    function restoreFontSize() {
        var fsStyle      = prefs.get("fontSize"),
            fsAdjustment = PreferencesManager.getViewState("fontSizeAdjustment");

        if (fsAdjustment) {
            // Always remove the old view state even if we also have the new view state.
            PreferencesManager.setViewState("fontSizeAdjustment");

            if (!fsStyle) {
                // Migrate the old view state to the new one.
                fsStyle = (DEFAULT_FONT_SIZE + fsAdjustment) + "px";
                prefs.set("fontSize", fsStyle);
            }
        }

        if (fsStyle) {
            _removeDynamicFontSize();
            _addDynamicFontSize(fsStyle);
        }
    }

    /**
     * Restores the font size and font family back to factory settings.
     */
    function restoreFonts() {
        setFontFamily(DEFAULT_FONT_FAMILY);
        setFontSize(DEFAULT_FONT_SIZE + "px");
    }


    /**
     * @private
     * Calculates the first and last visible lines of the focused editor
     * @param {number} textHeight
     * @param {number} scrollTop
     * @param {number} editorHeight
     * @return {{first: number, last: number}}
     */
    function _getLinesInView(textHeight, scrollTop, editorHeight) {
        var scrolledTop    = scrollTop / textHeight,
            scrolledBottom = (scrollTop + editorHeight) / textHeight;

        // Adjust the last line to round inward to show a whole lines.
        var firstLine      = Math.ceil(scrolledTop),
            lastLine       = Math.floor(scrolledBottom) - 1;

        return { first: firstLine, last: lastLine };
    }

    /**
     * @private
     * Scroll the viewport one line up or down.
     * @param {number} direction -1 to scroll one line up; 1 to scroll one line down.
     */
    function _scrollLine(direction) {
        var editor        = EditorManager.getCurrentFullEditor(),
            textHeight    = editor.getTextHeight(),
            cursorPos     = editor.getCursorPos(),
            hasSelecction = editor.hasSelection(),
            inlineEditors = editor.getInlineWidgets(),
            scrollInfo    = editor._codeMirror.getScrollInfo(),
            paddingTop    = editor._getLineSpaceElement().offsetTop,
            editorHeight  = scrollInfo.clientHeight,
            scrollTop     = scrollInfo.top - paddingTop,
            removedScroll = paddingTop;

        // Go through all the editors and reduce the scroll top and editor height to properly calculate the lines in view
        var line, coords;
        inlineEditors.forEach(function (inlineEditor) {
            line   = editor._getInlineWidgetLineNumber(inlineEditor);
            coords = editor._codeMirror.charCoords({line: line, ch: 0}, "local");

            if (coords.top < scrollInfo.top) {
                scrollTop     -= inlineEditor.info.height;
                removedScroll += inlineEditor.info.height;

            } else if (coords.top + inlineEditor.info.height < scrollInfo.top + editorHeight) {
                editorHeight -= inlineEditor.info.height;
            }
        });

        // Calculate the lines in view
        var linesInView = _getLinesInView(textHeight, scrollTop, editorHeight);

        // If there is no selection move the cursor so that is always visible.
        if (!hasSelecction) {
            // Move the cursor to the first visible line.
            if (cursorPos.line < linesInView.first) {
                editor.setCursorPos({line: linesInView.first + direction, ch: cursorPos.ch});

            // Move the cursor to the last visible line.
            } else if (cursorPos.line > linesInView.last) {
                editor.setCursorPos({line: linesInView.last + direction, ch: cursorPos.ch});

            // Move the cursor up or down using moveV to keep the goal column intact, since setCursorPos deletes it.
            } else if ((direction > 0 && cursorPos.line === linesInView.first) ||
                    (direction < 0 && cursorPos.line === linesInView.last)) {
                editor._codeMirror.moveV(direction, "line");
            }
        }

        // Scroll and make it snap to lines
        var lines = linesInView.first + direction;
        editor.setScrollPos(scrollInfo.left, (textHeight * lines) + removedScroll);
    }

    /** Scrolls one line up */
    function _handleScrollLineUp() {
        _scrollLine(-1);
    }

    /** Scrolls one line down */
    function _handleScrollLineDown() {
        _scrollLine(1);
    }

    /** Open theme settings dialog */
    function _handleThemeSettings() {
        ThemeSettings.showDialog();
    }

    // Register command handlers
    CommandManager.register(Strings.CMD_INCREASE_FONT_SIZE, Commands.VIEW_INCREASE_FONT_SIZE,  _handleIncreaseFontSize);
    CommandManager.register(Strings.CMD_DECREASE_FONT_SIZE, Commands.VIEW_DECREASE_FONT_SIZE,  _handleDecreaseFontSize);
    CommandManager.register(Strings.CMD_RESTORE_FONT_SIZE,  Commands.VIEW_RESTORE_FONT_SIZE,   _handleRestoreFontSize);
    CommandManager.register(Strings.CMD_SCROLL_LINE_UP,     Commands.VIEW_SCROLL_LINE_UP,      _handleScrollLineUp);
    CommandManager.register(Strings.CMD_SCROLL_LINE_DOWN,   Commands.VIEW_SCROLL_LINE_DOWN,    _handleScrollLineDown);
    CommandManager.register(Strings.CMD_THEMES,             Commands.CMD_THEMES_OPEN_SETTINGS, _handleThemeSettings);

    prefs.definePreference("fontSize",   "string", DEFAULT_FONT_SIZE + "px", {
        description: Strings.DESCRIPTION_FONT_SIZE
    }).on("change", function () {
        setFontSize(prefs.get("fontSize"));
    });
    prefs.definePreference("fontFamily", "string", DEFAULT_FONT_FAMILY, {
        description: Strings.DESCRIPTION_FONT_FAMILY
    }).on("change", function () {
        setFontFamily(prefs.get("fontFamily"));
    });

    // Define a preference for font smoothing mode on Mac.
    // By default fontSmoothing is set to "subpixel-antialiased"
    // for the text inside code editor. It can be overridden
    // to "antialiased", that would set text rendering AA to use
    // gray scale antialiasing.
    if (brackets.platform === "mac") {
        prefs.definePreference("fontSmoothing", "string", "subpixel-antialiased", {
            description: Strings.DESCRIPTION_FONT_SMOOTHING,
            values: ["subpixel-antialiased", "antialiased"]
        }).on("change", function () {
            setMacFontSmoothingType(prefs.get("fontSmoothing"));
        });
    }

    // Update UI when opening or closing a document
    MainViewManager.on("currentFileChange", _updateUI);

    // Update UI when Brackets finishes loading
    AppInit.appReady(init);

    EventDispatcher.makeEventDispatcher(exports);

    exports.restoreFontSize = restoreFontSize;
    exports.restoreFonts    = restoreFonts;
    exports.getFontSize     = getFontSize;
    exports.setFontSize     = setFontSize;
    exports.getFontFamily   = getFontFamily;
    exports.setFontFamily   = setFontFamily;
    exports.validFontSizeRegExp = validFontSizeRegExpStr;
});