diff --git a/src/command/CommandManager.js b/src/command/CommandManager.js index 7d788fb240c..282099b0a27 100644 --- a/src/command/CommandManager.js +++ b/src/command/CommandManager.js @@ -166,11 +166,12 @@ define(function (require, exports, module) { * execute() (after the id) are passed as arguments to the function. If the function is asynchronous, * it must return a jQuery promise that is resolved when the command completes. Otherwise, the * CommandManager will assume it is synchronous, and return a promise that is already resolved. - * @return {Command} + * @return {?Command} */ function register(name, id, commandFn) { if (_commands[id]) { - throw new Error("Attempting to register an already-registered command: " + id); + console.log("Attempting to register an already-registered command: " + id); + return null; } if (!name || !id || !commandFn) { throw new Error("Attempting to register a command with a missing name, id, or command function:" + name + " " + id); diff --git a/src/command/Menus.js b/src/command/Menus.js index 1ec93804ae6..30936f76b4c 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -23,7 +23,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, $, brackets, document */ +/*global define, $, brackets, window, MouseEvent */ define(function (require, exports, module) { 'use strict'; @@ -31,6 +31,7 @@ define(function (require, exports, module) { // Load dependent modules var Commands = require("command/Commands"), KeyBindingManager = require("command/KeyBindingManager"), + EditorManager = require("editor/EditorManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), CommandManager = require("command/CommandManager"); @@ -47,6 +48,15 @@ define(function (require, exports, module) { DEBUG_MENU: "debug-menu" }; + /** + * Brackets Context Menu Constants + * @enum {string} + */ + var ContextMenuIds = { + EDITOR_MENU: "editor-context-menu", + PROJECT_MENU: "project-context-menu" + }; + /** * Brackets Application Menu Section Constants @@ -93,6 +103,12 @@ define(function (require, exports, module) { */ var menuMap = {}; + /** + * Maps contextMenuID's to ContextMenu objects + * @type {Object.} + */ + var contextMenuMap = {}; + /** * Maps menuItemID's to MenuItem objects * @type {Object.} @@ -108,6 +124,15 @@ define(function (require, exports, module) { return menuMap[id]; } + /** + * Retrieves the ContextMenu object for the corresponding id. + * @param {string} id + * @return {ContextMenu} + */ + function getContextMenu(id) { + return contextMenuMap[id]; + } + /** * Retrieves the MenuItem object for the corresponding id. * @param {string} id @@ -118,11 +143,11 @@ define(function (require, exports, module) { } function _getHTMLMenu(id) { - return $("#" + id).get(0); + return $("#" + StringUtils.jQueryIdEscape(id)).get(0); } function _getHTMLMenuItem(id) { - return $("#" + id).get(0); + return $("#" + StringUtils.jQueryIdEscape(id)).get(0); } function _addKeyBindingToMenuItem($menuItem, key, displayKey) { @@ -249,6 +274,43 @@ define(function (require, exports, module) { this.id = id; } + /** + * Determine relative MenuItem + * + * @param {?string} relativeID - id of command (future: also sub-menu, or menu section). + */ + Menu.prototype._getRelativeMenuItem = function (relativeID) { + var $relativeElement, + key, + menuItem, + map, + foundMenuItem; + + if (relativeID) { + // Lookup Command for this Command id + var command = CommandManager.get(relativeID); + + if (command) { + // Find MenuItem that has this command + for (key in menuItemMap) { + if (menuItemMap.hasOwnProperty(key)) { + menuItem = menuItemMap[key]; + if (menuItem.getCommand() === command) { + foundMenuItem = menuItem; + break; + } + } + } + + if (foundMenuItem) { + $relativeElement = $(_getHTMLMenuItem(foundMenuItem.id)).closest("li"); + } + } + } + + return $relativeElement; + }; + /** * Adds a new menu item with the specified id and display text. The insertion position is * specified via the relativeID and position arguments which describe a position @@ -262,37 +324,34 @@ define(function (require, exports, module) { * Note, keyBindings are bound to Command objects not MenuItems. The provided keyBindings * will be bound to the supplied Command object rather than the MenuItem. * - * @param {!string} id * @param {!string | Command} command - the command the menu will execute. * Use DIVIDER for a menu divider * @param {?string | Array.<{key: string, platform: string}>} keyBindings - register one * one or more key bindings to associate with the supplied command. * @param {?string} position - constant defining the position of new the MenuItem relative * to other MenuItems. Default is LAST. (see Insertion position constants). - * @param {?string} relativeID - id of menuItem, sub-menu, or menu section that the new - * menuItem will be positioned relative to. Required when position is + * @param {?string} relativeID - id of command (future: also sub-menu, or menu section) that + * the new menuItem will be positioned relative to. Required when position is * AFTER or BEFORE, ignored when position is FIRST or LAST * * @return {MenuItem} the newly created MenuItem */ - Menu.prototype.addMenuItem = function (id, command, keyBindings, position, relativeID) { - var $menuItem, + Menu.prototype.addMenuItem = function (command, keyBindings, position, relativeID) { + var id, + $menuItem, $link, menuItem, name, commandID; - if (!id || !command) { - throw new Error("addMenuItem(): missing required parameters. id: " + id); - } - - if (menuItemMap[id]) { - throw new Error("MenuItem added with same id of existing MenuItem: " + id); + if (!command) { + throw new Error("addMenuItem(): missing required parameters: command"); } if (typeof (command) === "string") { if (command === DIVIDER) { name = DIVIDER; + commandID = _getNextMenuItemDividerID(); } else { commandID = command; command = CommandManager.get(commandID); @@ -301,6 +360,15 @@ define(function (require, exports, module) { } name = command.getName(); } + } else { + commandID = command.getID; + } + + // Internal id is the a composite of the parent menu id and the command id. + id = this.id + "-" + commandID; + + if (menuItemMap[id]) { + throw new Error("MenuItem added with same id of existing MenuItem: " + id); } // create MenuItem @@ -320,8 +388,9 @@ define(function (require, exports, module) { } // Insert menu item - var $relativeElement = relativeID && $(_getHTMLMenuItem(relativeID)).closest("li"); - _insertInList($("#main-toolbar li#" + this.id + " > ul.dropdown-menu"), $menuItem, position, $relativeElement); + var $relativeElement = this._getRelativeMenuItem(relativeID); + _insertInList($("li#" + StringUtils.jQueryIdEscape(this.id) + " > ul.dropdown-menu"), + $menuItem, position, $relativeElement); // Initialize MenuItem state if (!menuItem.isDivider) { @@ -357,7 +426,7 @@ define(function (require, exports, module) { * @return {MenuItem} the newly created divider */ Menu.prototype.addMenuDivider = function (position, relativeID) { - return this.addMenuItem(_getNextMenuItemDividerID(), DIVIDER, position, relativeID); + return this.addMenuItem(DIVIDER, position, relativeID); }; /** @@ -486,14 +555,16 @@ define(function (require, exports, module) { * Adds a top-level menu to the application menu bar which may be native or HTML-based. * * @param {!string} name - display text for menu - * @param {!string} id + * @param {!string} id - unique identifier for a menu. + * Core Menus in Brackets use a simple title as an id, for example "file-menu". + * Extensions should use the following format: "author.myextension.mymenuname". * @param {?string} position - constant defining the position of new the Menu relative * to other Menus. Default is LAST (see Insertion position constants). * * @param {?string} relativeID - id of Menu the new Menu will be positioned relative to. Required * when position is AFTER or BEFORE, ignored when position is FIRST or LAST * - * @return {Menu} the newly created Menu + * @return {?Menu} the newly created Menu */ function addMenu(name, id, position, relativeID) { name = StringUtils.htmlEscape(name); @@ -506,7 +577,8 @@ define(function (require, exports, module) { // Guard against duplicate menu ids if (menuMap[id]) { - throw new Error("Menu added with same name and id of existing Menu: " + id); + console.log("Menu added with same name and id of existing Menu: " + id); + return null; } menu = new Menu(id); @@ -524,6 +596,115 @@ define(function (require, exports, module) { return menu; } + + /** + * @constructor + * @extends {Menu} + * + * Represents a context menu that can open at a specific location in the UI. + * + * Clients should not create this object directly and should instead use registerContextMenu() + * to create new ContextMenu objects. + * + * Context menus in brackets may be HTML-based or native so clients should not reach into + * the HTML and should instead manipulate ContextMenus through the API. + * + * Events: + * beforeContextMenuOpen + * contextMenuClose + * + */ + function ContextMenu(id) { + this.id = id; + this.menu = new Menu(id); + + var $newMenu = $(""); + + var $toggle = $("") + .hide(); + + $newMenu.append($toggle) + .append(""); + + $("#context-menu-bar > ul").append($newMenu); + } + ContextMenu.prototype = new Menu(); + ContextMenu.prototype.constructor = ContextMenu; + ContextMenu.prototype.parentClass = Menu.prototype; + + + /** + * Displays the ContextMenu at the specified location and dispatches the + * "beforeContextMenuOpen" event.The menu location may be adjusted to prevent + * clipping by the browser window. All other menus and ContextMenus will be closed + * bofore a new menu is shown. + * + * @param {MouseEvent | {pageX:number, pageY:number}} mouseOrLocation - pass a MouseEvent + * to display the menu near the mouse or pass in an object with page x/y coordinates + * for a specific location. + */ + ContextMenu.prototype.open = function (mouseOrLocation) { + // TODO: positioning logic + + $(this).triggerHandler("beforeContextMenuOpen"); + + // close all other dropdowns + $(".dropdown").removeClass("open"); + + // open the context menu at specified location + + var menu = $("#" + StringUtils.jQueryIdEscape(this.id)); + // only show context menu if it has menu items + if (menu.find("ul").children().length > 0) { + menu.addClass("open") + .css({"left": mouseOrLocation.pageX, + "top": mouseOrLocation.pageY - 20}); + } + }; + + /** + * Closes the context menu and dispatches the "contextMenuClose" event. + */ + ContextMenu.prototype.close = function () { + $("#" + StringUtils.jQueryIdEscape(this.id)).removeClass("open"); + + $(this).triggerHandler("contextMenuClose"); + }; + + /** + * Registers new context menu with Brackets. + + * Extensions should generally use the predefined context menus built into Brackets. Use this + * API to add a new context menu specific to UI that is specific to an extension. + * + * After registering a context menu clients should: + * - use addMenuItem() to add items to the context menu + * - call open() to show the context menu (often trigged via an event handler for right click) + * + * To make menu items be contextual to things like selection, listen for the "beforeContextMenuOpen" + * to make changes to Command objects before the context menu is shown. MenuItems are views of + * Commands, which control a MenuItem's name, enabled state, and checked state. + * + * @param {string} id - unique identifier for context menu. + * Core context menus in Brackets use a simple title as an id. + * Extensions should use the following format: "author.myextension.mycontextmenu name" + * @return {?ContextMenu} the newly created context menu + */ + function registerContextMenu(id) { + if (!id) { + throw new Error("call to registerContextMenu() is missing required parameters"); + } + + // Guard against duplicate menu ids + if (contextMenuMap[id]) { + console.log("Context Menu added with same name and id of existing Context Menu: " + id); + return null; + } + + var cmenu = new ContextMenu(id); + contextMenuMap[id] = cmenu; + return cmenu; + } /** NOT IMPLEMENTED * Removes Menu @@ -540,100 +721,142 @@ define(function (require, exports, module) { */ var menu; menu = addMenu(Strings.FILE_MENU, AppMenuBar.FILE_MENU); - menu.addMenuItem("menu-file-new", Commands.FILE_NEW, "Ctrl-N"); - menu.addMenuItem("menu-file-open", Commands.FILE_OPEN, "Ctrl-O"); - menu.addMenuItem("menu-file-open-folder", Commands.FILE_OPEN_FOLDER); - menu.addMenuItem("menu-file-close", Commands.FILE_CLOSE, "Ctrl-W"); + menu.addMenuItem(Commands.FILE_NEW, "Ctrl-N"); + menu.addMenuItem(Commands.FILE_OPEN, "Ctrl-O"); + menu.addMenuItem(Commands.FILE_OPEN_FOLDER); + menu.addMenuItem(Commands.FILE_CLOSE, "Ctrl-W"); menu.addMenuDivider(); - menu.addMenuItem("menu-file-save", Commands.FILE_SAVE, "Ctrl-S"); + menu.addMenuItem(Commands.FILE_SAVE, "Ctrl-S"); menu.addMenuDivider(); - menu.addMenuItem("menu-file-live-preview", Commands.FILE_LIVE_FILE_PREVIEW, - "Ctrl-Alt-P"); + menu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW, "Ctrl-Alt-P"); menu.addMenuDivider(); - menu.addMenuItem("menu-file-quit", Commands.FILE_QUIT, "Ctrl-Q"); + menu.addMenuItem(Commands.FILE_QUIT, "Ctrl-Q"); /* * Edit menu */ menu = addMenu(Strings.EDIT_MENU, AppMenuBar.EDIT_MENU); - menu.addMenuItem("menu-edit-select-all", Commands.EDIT_SELECT_ALL, "Ctrl-A"); + menu.addMenuItem(Commands.EDIT_SELECT_ALL, "Ctrl-A"); menu.addMenuDivider(); - menu.addMenuItem("menu-edit-find", Commands.EDIT_FIND, "Ctrl-F"); - menu.addMenuItem("menu-edit-find-in-files", Commands.EDIT_FIND_IN_FILES, - "Ctrl-Shift-F"); - menu.addMenuItem("menu-edit-find-next", Commands.EDIT_FIND_NEXT, - [{key: "F3", platform: "win"}, - {key: "Ctrl-G", platform: "mac"}]); + menu.addMenuItem(Commands.EDIT_FIND, "Ctrl-F"); + menu.addMenuItem(Commands.EDIT_FIND_IN_FILES, "Ctrl-Shift-F"); + menu.addMenuItem(Commands.EDIT_FIND_NEXT, [{key: "F3", platform: "win"}, + {key: "Ctrl-G", platform: "mac"}]); - menu.addMenuItem("menu-edit-find-previous", Commands.EDIT_FIND_PREVIOUS, - [{key: "Shift-F3", platform: "win"}, - {key: "Ctrl-Shift-G", platform: "mac"}]); + menu.addMenuItem(Commands.EDIT_FIND_PREVIOUS, [{key: "Shift-F3", platform: "win"}, + {key: "Ctrl-Shift-G", platform: "mac"}]); menu.addMenuDivider(); - menu.addMenuItem("menu-edit-replace", Commands.EDIT_REPLACE, - [{key: "Ctrl-H", platform: "win"}, - {key: "Ctrl-Alt-F", platform: "mac"}]); + menu.addMenuItem(Commands.EDIT_REPLACE, [{key: "Ctrl-H", platform: "win"}, + {key: "Ctrl-Alt-F", platform: "mac"}]); menu.addMenuDivider(); - menu.addMenuItem("menu-edit-duplicate", Commands.EDIT_DUPLICATE, "Ctrl-D"); - menu.addMenuItem("menu-edit-comment", Commands.EDIT_LINE_COMMENT, "Ctrl-/"); + menu.addMenuItem(Commands.EDIT_DUPLICATE, "Ctrl-D"); + menu.addMenuItem(Commands.EDIT_LINE_COMMENT, "Ctrl-/"); /* * View menu */ menu = addMenu(Strings.VIEW_MENU, AppMenuBar.VIEW_MENU); - menu.addMenuItem("menu-view-sidebar", Commands.VIEW_HIDE_SIDEBAR, "Ctrl-Shift-H"); + menu.addMenuItem(Commands.VIEW_HIDE_SIDEBAR, "Ctrl-Shift-H"); menu.addMenuDivider(); - menu.addMenuItem("menu-view-increase-font", Commands.VIEW_INCREASE_FONT_SIZE, [{key: "Ctrl-=", displayKey: "Ctrl-+"}]); - menu.addMenuItem("menu-view-decrease-font", Commands.VIEW_DECREASE_FONT_SIZE, [{key: "Ctrl--", displayKey: "Ctrl-\u2212"}]); - menu.addMenuItem("menu-view-restore-font", Commands.VIEW_RESTORE_FONT_SIZE, "Ctrl-0"); + menu.addMenuItem(Commands.VIEW_INCREASE_FONT_SIZE, [{key: "Ctrl-=", displayKey: "Ctrl-+"}]); + menu.addMenuItem(Commands.VIEW_DECREASE_FONT_SIZE, [{key: "Ctrl--", displayKey: "Ctrl-\u2212"}]); + menu.addMenuItem(Commands.VIEW_RESTORE_FONT_SIZE, "Ctrl-0"); /* * Navigate menu */ menu = addMenu(Strings.NAVIGATE_MENU, AppMenuBar.NAVIGATE_MENU); - menu.addMenuItem("menu-navigate-quick-open", Commands.NAVIGATE_QUICK_OPEN, - "Ctrl-Shift-O"); - menu.addMenuItem("menu-navigate-goto-line", Commands.NAVIGATE_GOTO_LINE, - [{key: "Ctrl-G", platform: "win"}, - {key: "Ctrl-L", platform: "mac"}]); - - menu.addMenuItem("menu-navigate-goto-symbol", Commands.NAVIGATE_GOTO_DEFINITION, - "Ctrl-T"); + menu.addMenuItem(Commands.NAVIGATE_QUICK_OPEN, "Ctrl-Shift-O"); + menu.addMenuItem(Commands.NAVIGATE_GOTO_LINE, [{key: "Ctrl-G", platform: "win"}, + {key: "Ctrl-L", platform: "mac"}]); + + menu.addMenuItem(Commands.NAVIGATE_GOTO_DEFINITION, "Ctrl-T"); menu.addMenuDivider(); - menu.addMenuItem("menu-navigate-quick-edit", Commands.SHOW_INLINE_EDITOR, - "Ctrl-E"); - menu.addMenuItem("menu-navigate-prev-match", Commands.QUICK_EDIT_PREV_MATCH, - {key: "Alt-Up", displayKey: "Alt-\u2191"}); - menu.addMenuItem("menu-navigate-next-match", Commands.QUICK_EDIT_NEXT_MATCH, - {key: "Alt-Down", displayKey: "Alt-\u2193"}); + menu.addMenuItem(Commands.SHOW_INLINE_EDITOR, "Ctrl-E"); + menu.addMenuItem(Commands.QUICK_EDIT_PREV_MATCH, {key: "Alt-Up", displayKey: "Alt-\u2191"}); + menu.addMenuItem(Commands.QUICK_EDIT_NEXT_MATCH, {key: "Alt-Down", displayKey: "Alt-\u2193"}); + /* * Debug menu */ menu = addMenu(Strings.DEBUG_MENU, AppMenuBar.DEBUG_MENU); - menu.addMenuItem("menu-debug-reload-wn", Commands.DEBUG_REFRESH_WINDOW, - [{key: "F5", platform: "win"}, - {key: "Ctrl-R", platform: "mac"}]); - - menu.addMenuItem("menu-debug-dev-tools", Commands.DEBUG_SHOW_DEVELOPER_TOOLS); - menu.addMenuItem("menu-debug-run-tests", Commands.DEBUG_RUN_UNIT_TESTS); - menu.addMenuItem("menu-debug-enable-jslint", Commands.DEBUG_JSLINT); - menu.addMenuItem("menu-debug-perf-data", Commands.DEBUG_SHOW_PERF_DATA); + menu.addMenuItem(Commands.DEBUG_REFRESH_WINDOW, [{key: "F5", platform: "win"}, + {key: "Ctrl-R", platform: "mac"}]); + + menu.addMenuItem(Commands.DEBUG_SHOW_DEVELOPER_TOOLS); + menu.addMenuItem(Commands.DEBUG_RUN_UNIT_TESTS); + menu.addMenuItem(Commands.DEBUG_JSLINT); + menu.addMenuItem(Commands.DEBUG_SHOW_PERF_DATA); menu.addMenuDivider(); - menu.addMenuItem("menu-debug-experiemental", Commands.DEBUG_EXPERIMENTAL); - menu.addMenuItem("menu-debug-new-window", Commands.DEBUG_NEW_BRACKETS_WINDOW); - menu.addMenuItem("menu-debug-close-browser", Commands.DEBUG_CLOSE_ALL_LIVE_BROWSERS); - menu.addMenuItem("menu-debug-use-tab-chars", Commands.DEBUG_USE_TAB_CHARS); - + menu.addMenuItem(Commands.DEBUG_EXPERIMENTAL); + menu.addMenuItem(Commands.DEBUG_NEW_BRACKETS_WINDOW); + menu.addMenuItem(Commands.DEBUG_CLOSE_ALL_LIVE_BROWSERS); + menu.addMenuItem(Commands.DEBUG_USE_TAB_CHARS); + + + /* + * Context Menus + */ + var project_cmenu = registerContextMenu(ContextMenuIds.PROJECT_MENU); + + var editor_cmenu = registerContextMenu(ContextMenuIds.EDITOR_MENU); + editor_cmenu.addMenuItem(Commands.SHOW_INLINE_EDITOR); + editor_cmenu.addMenuItem(Commands.EDIT_SELECT_ALL); + + /** + * Displays context menu when right clicking editor. + * Auto selects the word the user clicks if the click does not occur over + * an existing selection + * + * TODO: doesn't word select when changing editors with right click + * + */ + $("#editor-holder").mousedown(function (e) { + if (e.which === 3) { + if ($(e.target).parents(".CodeMirror-gutter").length !== 0) { + return; + } + + var editor = EditorManager.getFocusedEditor(); + var clickedSel = false, + pos = editor.coordsChar({x: e.pageX, y: e.pageY}); + if (editor.getSelectedText() !== "") { + var sel = editor.getSelection(); + clickedSel = editor.coordsWithinRange(pos, sel.start, sel.end); + } + + if (!clickedSel) { + editor.selectWordAt(pos); + } + editor_cmenu.open(e); + } + }); + + + $("#projects").mousedown(function (e) { + if (e.which === 3) { + project_cmenu.open(e); + } + }); + // Prevent clicks on the top-level menu bar from taking focus - // Note, bootstrap handles this already for the menu drop downs - $(document).on("mousedown", "#main-toolbar .dropdown", function (e) { + // Note, bootstrap handles this already for the menu drop downs + $(window.document).on("mousedown", ".dropdown", function (e) { e.preventDefault(); }); - + + // close all dropdowns on ESC + $(window.document).on("keydown", function (e) { + if (e.keyCode === 27) { + $(".dropdown").removeClass("open"); + } + }); + // Switch menus when the mouse enters an adjacent menu // Only open the menu if another one has already been opened // by clicking - $(document).on("mouseenter", "#main-toolbar .dropdown", function (e) { + $(window.document).on("mouseenter", "#main-toolbar .dropdown", function (e) { var open = $(this).siblings(".open"); if (open.length > 0) { open.removeClass("open"); @@ -645,6 +868,7 @@ define(function (require, exports, module) { // Define public API exports.init = init; exports.AppMenuBar = AppMenuBar; + exports.ContextMenuIds = ContextMenuIds; exports.MenuSection = MenuSection; exports.BEFORE = BEFORE; exports.AFTER = AFTER; @@ -653,7 +877,10 @@ define(function (require, exports, module) { exports.DIVIDER = DIVIDER; exports.getMenu = getMenu; exports.getMenuItem = getMenuItem; + exports.getContextMenu = getContextMenu; exports.addMenu = addMenu; exports.Menu = Menu; exports.MenuItem = MenuItem; + exports.registerContextMenu = registerContextMenu; + exports.ContextMenu = ContextMenu; }); diff --git a/src/editor/Editor.js b/src/editor/Editor.js index 4d3e6ffcf11..4df468ead88 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -673,7 +673,34 @@ define(function (require, exports, module) { Editor.prototype.setCursorPos = function (line, ch) { this._codeMirror.setCursor(line, ch); }; - + + /** + * @param {line:number, ch:number} + * @return {number} + */ + Editor.prototype.indexFromPos = function (coords) { + return this._codeMirror.indexFromPos(coords); + }; + + /** + * Returns true if coords is between start and end (inclusive) + * @param {line:number, ch:number} coords + * @param {line:number, ch:number} start + * @param {line:number, ch:number} end + * + */ + Editor.prototype.coordsWithinRange = function (coords, start, end) { + var startIndex = this.indexFromPos(start), + endIndex = this.indexFromPos(end), + coordIndex = this.indexFromPos(coords); + + return coordIndex >= startIndex && coordIndex <= endIndex; + }; + + Editor.prototype.coordsChar = function (coords) { + return this._codeMirror.coordsChar(coords); + }; + /** * Gets the current selection. Start is inclusive, end is exclusive. If there is no selection, * returns the current cursor position as both the start and end of the range (i.e. a selection @@ -704,6 +731,14 @@ define(function (require, exports, module) { this._codeMirror.setSelection(start, end); }; + /** + * Selects a the word nearest to the position + * @param {line:number, ch:number} + */ + Editor.prototype.selectWordAt = function (pos) { + this._codeMirror.selectWordAt(pos); + }; + /** * Gets the total number of lines in the the document (includes lines not visible in the viewport) diff --git a/src/editor/InlineTextEditor.js b/src/editor/InlineTextEditor.js index 0658806a6b6..a1bedf54fce 100644 --- a/src/editor/InlineTextEditor.js +++ b/src/editor/InlineTextEditor.js @@ -73,7 +73,7 @@ define(function (require, exports, module) { /** * @constructor - * + * @extends {InlineWidget} */ function InlineTextEditor() { InlineWidget.call(this); diff --git a/src/extensions/disabled/ContextMenuTest/main.js b/src/extensions/disabled/ContextMenuTest/main.js new file mode 100644 index 00000000000..2dbe8a83524 --- /dev/null +++ b/src/extensions/disabled/ContextMenuTest/main.js @@ -0,0 +1,103 @@ +/* + * 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, $ */ + +define(function (require, exports, module) { + 'use strict'; + + // Brackets modules + var CommandManager = brackets.getModule("command/CommandManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + DocumentManager = brackets.getModule("document/DocumentManager"), + Menus = brackets.getModule("command/Menus"); + + function TestCommand1() { + var command1 = CommandManager.get("custom.command1"); + if (!command1) { + return; + } + var command2 = CommandManager.get("custom.command2"); + if (!command2) { + return; + } + + var checked = command1.getChecked(); + if (checked) { + alert("Unchecking self. Disabling next."); + command2.setEnabled(false); + } else { + alert("Checking self. Enabling next."); + command2.setEnabled(true); + } + command1.setChecked(!checked); + } + + function TestCommand2() { + alert("Executing command 2"); + } + + function TestCommand3() { + alert("Executing command 3"); + } + + // Create command + var command1 = CommandManager.register("Toggle Checkmark", "custom.command1", TestCommand1); + var command2 = CommandManager.register("Enabled when previous is Checked", "custom.command2", TestCommand2); + var command3 = CommandManager.register("Enabled when text selected", "custom.command3", TestCommand3); + + command1.setChecked(true); + command2.setEnabled(true); + command3.setEnabled(false); + + + var handleDocChanged = function () { + var editor = EditorManager.getFocusedEditor(); + + var handleEnableState = function () { + command3.setEnabled(editor.getSelectedText() !== ""); + }; + + if (editor) { + $(editor).off("cursorActivity", handleEnableState); + $(editor).on("cursorActivity", handleEnableState); + } + }; + + $(DocumentManager).on("currentDocumentChange", handleDocChanged); + handleDocChanged(); + + + // Get editor context menu + var editor_cmenu = Menus.getContextMenu(Menus.ContextMenuIds.EDITOR_MENU); + + // Add our MenuItem at the end + if (editor_cmenu) { + editor_cmenu.addMenuDivider(); + editor_cmenu.addMenuItem("custom.command1"); + editor_cmenu.addMenuItem("custom.command2"); + editor_cmenu.addMenuItem("custom.command3"); + } +}); diff --git a/src/index.html b/src/index.html index e46ad49803e..aa3d00bd57e 100644 --- a/src/index.html +++ b/src/index.html @@ -210,5 +210,8 @@

Brackets

Close +
+
    +
    diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index 3907bf4ca92..d99e4d81645 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -167,7 +167,7 @@ define(function (require, exports, module) { _updateFileStatusIcon($newItem, isOpenAndDirty(file), false); _updateListItemSelection($newItem, curDoc); - $newItem.click(function () { + $newItem.mousedown(function () { FileViewController.openAndSelectDocument(file.fullPath, FileViewController.WORKING_SET_VIEW); }); diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 3ed27a3d40c..3abc866d104 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -160,7 +160,7 @@ /* Menu-related styles */ -.toolbar .nav { +.toolbar .nav, .context-menu { @menubar-top-padding: 8px; @menubar-bottom-padding: 6px; @menubar-h-padding: 9px; @@ -258,6 +258,26 @@ } } +/* Context menu styles */ + +.context-menu { + position: absolute; + z-index: @z-index-brackets-context-menu-base; + list-style-type: none; + + .dropdown-menu { + .border-radius(3px); + } + + .menu-shortcut { + float: right; + } +} + +#context-menu-bar { + margin: 0; +} + /* Dialog-related styles */ .modal-footer .btn.left { diff --git a/src/styles/brackets_variables.less b/src/styles/brackets_variables.less index 03fe1247c74..170bf59b3f4 100644 --- a/src/styles/brackets_variables.less +++ b/src/styles/brackets_variables.less @@ -50,3 +50,5 @@ @z-index-brackets-sidebar-resizer: @z-index-brackets-ui + 2; @z-index-brackets-resizer-div: @z-index-brackets-sidebar-resizer + 1; + +@z-index-brackets-context-menu-base: 1000; diff --git a/src/utils/StringUtils.js b/src/utils/StringUtils.js index 52209fdf0dd..6b54d9d06c3 100644 --- a/src/utils/StringUtils.js +++ b/src/utils/StringUtils.js @@ -62,6 +62,12 @@ define(function (require, exports, module) { return str.replace(/([.?*+\^$\[\]\\(){}|\-])/g, "\\$1"); } + // Periods (aka "dots") are allowed in HTML identifiers, but jQuery interprets + // them as the start of a class selector, so they need to be escaped + function jQueryIdEscape(str) { + return str.replace(/\./g, "\\."); + } + /** * Splits the text by new line characters and returns an array of lines * @param {string} text @@ -117,6 +123,7 @@ define(function (require, exports, module) { exports.format = format; exports.htmlEscape = htmlEscape; exports.regexEscape = regexEscape; + exports.jQueryIdEscape = jQueryIdEscape; exports.getLines = getLines; exports.offsetToLineNum = offsetToLineNum; -}); \ No newline at end of file +}); diff --git a/test/spec/CommandManager-test.js b/test/spec/CommandManager-test.js index fb9354b43d4..b5e9bf5caa2 100644 --- a/test/spec/CommandManager-test.js +++ b/test/spec/CommandManager-test.js @@ -57,7 +57,7 @@ define(function (require, exports, module) { expect(command._commandFn).toBe(testCommandFn); // duplicate command - expect(function () { CommandManager.register("test command", commandID, testCommandFn); }).toThrow(); + expect(CommandManager.register("test command", commandID, testCommandFn)).toBeFalsy(); // missing arguments expect(function () { CommandManager.register(null, "test-command-id2", testCommandFn); }).toThrow(); diff --git a/test/spec/Menu-test.js b/test/spec/Menu-test.js index c1c6f197c97..f8edad231a2 100644 --- a/test/spec/Menu-test.js +++ b/test/spec/Menu-test.js @@ -32,6 +32,7 @@ define(function (require, exports, module) { KeyBindingManager, Menus, SpecRunnerUtils = require("./SpecRunnerUtils.js"), + StringsUtils = require("utils/StringUtils"), Strings = require("strings"); describe("Menus", function () { @@ -150,13 +151,9 @@ define(function (require, exports, module) { expect(menu1).not.toBeNull(); var menu2 = null; - var exceptionThrown = false; - try { - menu2 = Menus.addMenu("Custom", "menu-custom"); - } catch (e) { - exceptionThrown = true; - } - expect(exceptionThrown).toBeTruthy(); + + menu2 = Menus.addMenu("Custom", "menu-custom"); + expect(menu2).toBeFalsy(); $listItems = testWindow.$("#main-toolbar > ul.nav").children(); expect($listItems.length).toBe(menuCountOriginal + 1); @@ -176,14 +173,17 @@ define(function (require, exports, module) { expect($listItems.length).toBe(0); // Re-use commands that are already registered - var menuItem = menu.addMenuItem("menuitem-custom", "custom.command"); + var menuItem = menu.addMenuItem("custom.command"); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$("#menu-custom > ul").children(); expect($listItems.length).toBe(1); expect($($listItems[0]).length).toBe(1); - expect($($listItems[0]).find("a#menuitem-custom").length).toBe(1); + + // Periods (aka "dots") are allowed in HTML identifiers, but jQuery interprets + // them as the start of a class selector, so they need to be escaped + expect($($listItems[0]).find("a#menu-custom-custom\\.command").length).toBe(1); }); }); @@ -192,18 +192,18 @@ define(function (require, exports, module) { CommandManager.register("Command Custom 0", "custom.command0", function () {}); CommandManager.register("Command Custom 1", "custom.command1", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); + var menuItem = menu.addMenuItem("custom.command0"); var listSelector = "#menu-custom > ul"; var $listItems = testWindow.$(listSelector).children(); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1", "Ctrl-Alt-0", Menus.FIRST); + menuItem = menu.addMenuItem("custom.command1", "Ctrl-Alt-0", Menus.FIRST); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$(listSelector).children(); expect($listItems.length).toBe(2); - expect($($listItems[0]).find("a#menuitem-custom-1").length).toBe(1); + expect($($listItems[0]).find("a#menu-custom-custom\\.command1").length).toBe(1); }); }); @@ -212,18 +212,18 @@ define(function (require, exports, module) { CommandManager.register("Command Custom 0", "custom.command0", function () {}); CommandManager.register("Command Custom 1", "custom.command1", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); + var menuItem = menu.addMenuItem("custom.command0"); var listSelector = "#menu-custom > ul"; var $listItems = testWindow.$(listSelector).children(); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1", Menus.LAST); + menuItem = menu.addMenuItem("custom.command1", Menus.LAST); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$(listSelector).children(); expect($listItems.length).toBe(2); - expect($($listItems[1]).find("a#menuitem-custom-1").length).toBe(1); + expect($($listItems[1]).find("a#menu-custom-custom\\.command1").length).toBe(1); }); }); @@ -233,19 +233,19 @@ define(function (require, exports, module) { CommandManager.register("Command Custom 1", "custom.command1", function () {}); CommandManager.register("Command Custom 2", "custom.command2", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0", "Ctrl-Alt-0"); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1", "Ctrl-Alt-1"); + var menuItem = menu.addMenuItem("custom.command0", "Ctrl-Alt-0"); + menuItem = menu.addMenuItem("custom.command1", "Ctrl-Alt-1"); var listSelector = "#menu-custom > ul"; var $listItems = testWindow.$(listSelector).children(); - menuItem = menu.addMenuItem("menuitem-custom-2", "custom.command2", "Ctrl-Alt-2", Menus.AFTER, "menuitem-custom-0"); + menuItem = menu.addMenuItem("custom.command2", "Ctrl-Alt-2", Menus.AFTER, "custom.command0"); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$(listSelector).children(); expect($listItems.length).toBe(3); - expect($($listItems[1]).find("a#menuitem-custom-2").length).toBe(1); + expect($($listItems[1]).find("a#menu-custom-custom\\.command2").length).toBe(1); }); }); @@ -255,19 +255,19 @@ define(function (require, exports, module) { CommandManager.register("Command Custom 1", "custom.command1", function () {}); CommandManager.register("Command Custom 2", "custom.command2", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0", "Ctrl-Alt-0"); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1", "Ctrl-Alt-1"); + var menuItem = menu.addMenuItem("custom.command0", "Ctrl-Alt-0"); + menuItem = menu.addMenuItem("custom.command1", "Ctrl-Alt-1"); var listSelector = "#menu-custom > ul"; var $listItems = testWindow.$(listSelector).children(); - menuItem = menu.addMenuItem("menuitem-custom-2", "custom.command2", "Ctrl-Alt-2", Menus.BEFORE, "menuitem-custom-1"); + menuItem = menu.addMenuItem("custom.command2", "Ctrl-Alt-2", Menus.BEFORE, "custom.command1"); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$(listSelector).children(); expect($listItems.length).toBe(3); - expect($($listItems[1]).find("a#menuitem-custom-2").length).toBe(1); + expect($($listItems[1]).find("a#menu-custom-custom\\.command2").length).toBe(1); }); }); @@ -277,29 +277,28 @@ define(function (require, exports, module) { CommandManager.register("Command Custom 1", "custom.command1", function () {}); CommandManager.register("Command Custom 2", "custom.command2", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0", "Ctrl-Alt-0"); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1", "Ctrl-Alt-1"); + var menuItem = menu.addMenuItem("custom.command0", "Ctrl-Alt-0"); + menuItem = menu.addMenuItem("custom.command1", "Ctrl-Alt-1"); var listSelector = "#menu-custom > ul"; var $listItems = testWindow.$(listSelector).children(); - menuItem = menu.addMenuItem("menuitem-custom-2", "custom.command2", "Ctrl-Alt-2", Menus.BEFORE, "NONEXISTANT"); + menuItem = menu.addMenuItem("custom.command2", "Ctrl-Alt-2", Menus.BEFORE, "NONEXISTANT"); expect(menuItem).not.toBeNull(); expect(menuItem).toBeDefined(); $listItems = testWindow.$(listSelector).children(); expect($listItems.length).toBe(3); - expect($($listItems[2]).find("a#menuitem-custom-2").length).toBe(1); + expect($($listItems[2]).find("a#menu-custom-custom\\.command2").length).toBe(1); }); }); - it("should not add duplicate menu item", function () { + it("should not add menu item for duplicate command in a menu", function () { runs(function () { CommandManager.register("Command Custom 0", "custom.command0", function () {}); - CommandManager.register("Command Custom 1", "custom.command1", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); + var menuItem = menu.addMenuItem("custom.command0"); var $listItems = testWindow.$("#menu-custom > ul").children(); expect($listItems.length).toBe(1); @@ -307,7 +306,7 @@ define(function (require, exports, module) { var exceptionThrown = false; try { menuItem = null; - menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command1"); + menuItem = menu.addMenuItem("custom.command0"); } catch (e) { exceptionThrown = true; } @@ -330,7 +329,7 @@ define(function (require, exports, module) { var menuItem = null; var exceptionThrown = false; try { - menuItem = menu.addMenuItem("menuitem-custom-0", "UNREGISTERED_COMMAND"); + menuItem = menu.addMenuItem("UNREGISTERED_COMMAND"); } catch (e) { exceptionThrown = true; } @@ -350,9 +349,9 @@ define(function (require, exports, module) { var menu = Menus.addMenu("Custom", "menu-custom"); var menuItem = null; - menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); + menuItem = menu.addMenuItem("custom.command0"); menuItem = menu.addMenuDivider(); - menuItem = menu.addMenuItem("menuitem-custom-1", "custom.command1"); + menuItem = menu.addMenuItem("custom.command1"); var $listItems = testWindow.$("#menu-custom > ul").children(); expect($listItems.length).toBe(3); @@ -370,8 +369,8 @@ define(function (require, exports, module) { expect(cmd).toBeDefined(); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); - var menuSelector = "#menuitem-custom-0"; + var menuItem = menu.addMenuItem("custom.command0"); + var menuSelector = "#menu-custom-custom\\.command0"; // Verify menu is synced with command var $menuItem = testWindow.$(menuSelector); @@ -401,8 +400,8 @@ define(function (require, exports, module) { expect(cmd).toBeDefined(); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0"); - var menuSelector = "#menuitem-custom-0"; + var menuItem = menu.addMenuItem("custom.command0"); + var menuSelector = "#menu-custom-custom\\.command0"; // Verify menu is synced with command var $menuItem = testWindow.$(menuSelector); @@ -428,8 +427,8 @@ define(function (require, exports, module) { runs(function () { CommandManager.register("Command Custom 0", "custom.command0", function () {}); var menu = Menus.addMenu("Custom", "menu-custom"); - var menuItem = menu.addMenuItem("menuitem-custom-0", "custom.command0", "Ctrl-9"); - var menuSelector = "#menuitem-custom-0"; + var menuItem = menu.addMenuItem("custom.command0", "Ctrl-9"); + var menuSelector = "#menu-custom-custom\\.command0"; // Verify menu is synced with command var $menuItem = testWindow.$(menuSelector),