From 2e5e841cd5d19907ce9a6ae418ae969326055e0d Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Mon, 16 Sep 2013 16:29:33 -0700 Subject: [PATCH 1/9] Pull logic for context menus into render process --- src/atom-window.coffee | 14 +++----------- src/context-menu.coffee | 8 ++++++++ src/window-event-handler.coffee | 5 ++++- 3 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 src/context-menu.coffee diff --git a/src/atom-window.coffee b/src/atom-window.coffee index c2cf61799..dc7918bcf 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -1,6 +1,7 @@ BrowserWindow = require 'browser-window' Menu = require 'menu' MenuItem = require 'menu-item' +ContextMenu = require 'context-menu' app = require 'app' dialog = require 'dialog' ipc = require 'ipc' @@ -12,8 +13,6 @@ _ = require 'underscore' module.exports = class AtomWindow browserWindow: null - contextMenu: null - inspectElementMenuItem: null loaded: null isSpec: null @@ -22,7 +21,6 @@ class AtomWindow global.atomApplication.addWindow(this) @setupNodePath(@resourcePath) - @createContextMenu() @browserWindow = new BrowserWindow show: false, title: 'Atom' @browserWindow.restart = _.wrap _.bind(@browserWindow.restart, @browserWindow), (restart) => @setupNodePath(@resourcePath) @@ -95,9 +93,8 @@ class AtomWindow when 0 then @browserWindow.destroy() when 1 then @browserWindow.restart() - @browserWindow.on 'context-menu', (x, y) => - @inspectElementMenuItem.click = => @browserWindow.inspectElement(x, y) - @contextMenu.popup(@browserWindow) + @browserWindow.on 'context-menu', (menuTemplate) => + new ContextMenu(menuTemplate) if @isSpec # Spec window's web view should always have focus @@ -111,11 +108,6 @@ class AtomWindow else @browserWindow.once 'window:loaded', => @openPath(pathToOpen, initialLine) - createContextMenu: -> - @contextMenu = new Menu - @inspectElementMenuItem = new MenuItem(label: 'Inspect Element') - @contextMenu.append(@inspectElementMenuItem) - sendCommand: (command, args...) -> if @handlesAtomCommands() @sendAtomCommand(command, args...) diff --git a/src/context-menu.coffee b/src/context-menu.coffee new file mode 100644 index 000000000..3069bb3c4 --- /dev/null +++ b/src/context-menu.coffee @@ -0,0 +1,8 @@ +Menu = require 'menu' +BrowserWindow = require 'browser-window' + +module.exports = +class ContextMenu + constructor: (template) -> + menu = Menu.buildFromTemplate template + menu.popup(BrowserWindow.getFocusedWindow()) diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index da2cf8a3c..c6dbadd17 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -39,7 +39,10 @@ class WindowEventHandler @subscribe $(document), 'contextmenu', (e) -> e.preventDefault() - remote.getCurrentWindow().emit('context-menu', e.pageX, e.pageY) + menuTemplate = [ + { label: 'Inspect Element', click: -> remote.getCurrentWindow().inspectElement(e.pageX, e.pageY) } + ] + remote.getCurrentWindow().emit('context-menu', menuTemplate) openLink: (event) => location = $(event.target).attr('href') From 9e11f5a73759be7f304b030894e6168c6ec3f451 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Mon, 16 Sep 2013 17:26:40 -0700 Subject: [PATCH 2/9] Add ContextMenuMap and use it to create the context menu --- src/atom.coffee | 2 ++ src/context-menu-map.coffee | 47 +++++++++++++++++++++++++++++++++ src/window-event-handler.coffee | 10 ++++--- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/context-menu-map.coffee diff --git a/src/atom.coffee b/src/atom.coffee index 422949da5..cab272df9 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -10,12 +10,14 @@ dialog = remote.require 'dialog' app = remote.require 'app' telepath = require 'telepath' ThemeManager = require 'theme-manager' +ContextMenuMap = require 'context-menu-map' window.atom = loadedPackages: {} activePackages: {} packageStates: {} themes: new ThemeManager() + contextMenuMap: new ContextMenuMap() getLoadSettings: -> remote.getCurrentWindow().loadSettings diff --git a/src/context-menu-map.coffee b/src/context-menu-map.coffee new file mode 100644 index 000000000..905df220a --- /dev/null +++ b/src/context-menu-map.coffee @@ -0,0 +1,47 @@ +$ = require 'jquery' + +# Public: Provides a registry for commands that you'd wish to appear in the +# context menu. +# +# Should be accessed via `atom.contextMenuMap`. +module.exports = +class ContextMenuMap + # Private: + constructor: -> + @mappings = {} + @devModeMappings = {} + + # Public: Registers a command to be displayed when the relevant item is right + # clicked. + # + # * selector: The css selector for the active element which should include + # the given command in it's context menu. + # * label: The text that should appear in the context menu. + # * command: The command string that should be triggered on the activeElement + # which matches your selector. + # * options: + # + devMode: Indicates whether this command should only appear while the + # editor is in dev mode. + add: (selector, label, command, {devMode}={}) -> + mappings = if devMode then @devModeMappings else @mappings + mappings[selector] ?= [] + mappings[selector].push({label, command}) + + # Private: + bindingsForElement: (element, {devMode}={}) -> + mappings = if devMode then @devModeMappings else @mappings + items for selector, items of mappings when element.webkitMatchesSelector(selector) + + # Public: Used to generate the context menu for a specific element. + # + # * element: The DOM element to generate the menu template for. + menuTemplateForElement: (element) -> + menuTemplate = [] + for devMode in [false, true] + for items in @bindingsForElement(element, {devMode}) + for {label, command} in items + template = {label} + template.click = -> $(element).trigger(command) + menuTemplate.push(template) + + menuTemplate diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index c6dbadd17..78107ed34 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -39,9 +39,13 @@ class WindowEventHandler @subscribe $(document), 'contextmenu', (e) -> e.preventDefault() - menuTemplate = [ - { label: 'Inspect Element', click: -> remote.getCurrentWindow().inspectElement(e.pageX, e.pageY) } - ] + menuTemplate = atom.contextMenuMap.menuTemplateForElement(e.target) + + # FIXME: This should be registered as a dev binding on + # atom.contextMenuMapping, but I'm not sure where in the source. + menuTemplate.push({ type: 'separator' }) + menuTemplate.push({ label: 'Inspect Element', click: -> remote.getCurrentWindow().inspectElement(e.pageX, e.pageY) }) + remote.getCurrentWindow().emit('context-menu', menuTemplate) openLink: (event) => From 63f8631fd18d8caa5d7a33130655c77cb495d017 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Tue, 17 Sep 2013 14:14:34 -0700 Subject: [PATCH 3/9] Minor grammar improvements --- src/context-menu-map.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context-menu-map.coffee b/src/context-menu-map.coffee index 905df220a..7b6b51460 100644 --- a/src/context-menu-map.coffee +++ b/src/context-menu-map.coffee @@ -1,6 +1,6 @@ $ = require 'jquery' -# Public: Provides a registry for commands that you'd wish to appear in the +# Public: Provides a registry for commands that you'd like to appear in the # context menu. # # Should be accessed via `atom.contextMenuMap`. @@ -15,7 +15,7 @@ class ContextMenuMap # clicked. # # * selector: The css selector for the active element which should include - # the given command in it's context menu. + # the given command in its context menu. # * label: The text that should appear in the context menu. # * command: The command string that should be triggered on the activeElement # which matches your selector. From 2f419a639cd9294a9f0456fb7868053ea89b6b37 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Tue, 17 Sep 2013 14:32:37 -0700 Subject: [PATCH 4/9] s/ContextMenuMap/ContextMenu/g --- src/atom.coffee | 4 ++-- src/{context-menu-map.coffee => context-menu-manager.coffee} | 4 ++-- src/window-event-handler.coffee | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/{context-menu-map.coffee => context-menu-manager.coffee} (95%) diff --git a/src/atom.coffee b/src/atom.coffee index cab272df9..985d3662a 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -10,14 +10,14 @@ dialog = remote.require 'dialog' app = remote.require 'app' telepath = require 'telepath' ThemeManager = require 'theme-manager' -ContextMenuMap = require 'context-menu-map' +ContextMenuManager = require 'context-menu-manager' window.atom = loadedPackages: {} activePackages: {} packageStates: {} themes: new ThemeManager() - contextMenuMap: new ContextMenuMap() + contextMenu: new ContextMenuManager() getLoadSettings: -> remote.getCurrentWindow().loadSettings diff --git a/src/context-menu-map.coffee b/src/context-menu-manager.coffee similarity index 95% rename from src/context-menu-map.coffee rename to src/context-menu-manager.coffee index 7b6b51460..6f09dc80b 100644 --- a/src/context-menu-map.coffee +++ b/src/context-menu-manager.coffee @@ -3,9 +3,9 @@ $ = require 'jquery' # Public: Provides a registry for commands that you'd like to appear in the # context menu. # -# Should be accessed via `atom.contextMenuMap`. +# Should be accessed via `atom.contextMenu`. module.exports = -class ContextMenuMap +class ContextMenuManager # Private: constructor: -> @mappings = {} diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 78107ed34..049b72548 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -39,10 +39,10 @@ class WindowEventHandler @subscribe $(document), 'contextmenu', (e) -> e.preventDefault() - menuTemplate = atom.contextMenuMap.menuTemplateForElement(e.target) + menuTemplate = atom.contextMenu.menuTemplateForElement(e.target) # FIXME: This should be registered as a dev binding on - # atom.contextMenuMapping, but I'm not sure where in the source. + # atom.contextMenu, but I'm not sure where in the source. menuTemplate.push({ type: 'separator' }) menuTemplate.push({ label: 'Inspect Element', click: -> remote.getCurrentWindow().inspectElement(e.pageX, e.pageY) }) From 037d39e943a3373170b2a1f583b94b600288fa04 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 18 Sep 2013 09:53:57 -0700 Subject: [PATCH 5/9] Rewrite based on feedback --- src/atom-application.coffee | 1 + src/atom-window.coffee | 3 +- src/context-menu-manager.coffee | 82 ++++++++++++++++++++++++--------- src/context-menu.coffee | 14 +++++- src/window-event-handler.coffee | 12 ++--- 5 files changed, 79 insertions(+), 33 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 54f2fa92b..e02ab2acb 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -129,6 +129,7 @@ class AtomApplication @on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:') @on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:') @on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:') + @on 'application:inspect', ({x,y}) -> @focusedWindow().browserWindow.inspectElement(x, y) app.on 'will-quit', => fs.unlinkSync socketPath if fs.existsSync(socketPath) # Clean the socket file when quit normally. diff --git a/src/atom-window.coffee b/src/atom-window.coffee index dc7918bcf..cae167f10 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -115,7 +115,8 @@ class AtomWindow @sendNativeCommand(command) sendAtomCommand: (command, args...) -> - ipc.sendChannel @browserWindow.getProcessId(), @browserWindow.getRoutingId(), 'command', command, args... + action = if args[0]?.contextCommand then 'context-command' else 'command' + ipc.sendChannel @browserWindow.getProcessId(), @browserWindow.getRoutingId(), action, command, args... sendNativeCommand: (command) -> switch command diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 6f09dc80b..58f81014a 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -1,4 +1,6 @@ $ = require 'jquery' +_ = require 'underscore' +remote = require 'remote' # Public: Provides a registry for commands that you'd like to appear in the # context menu. @@ -8,40 +10,74 @@ module.exports = class ContextMenuManager # Private: constructor: -> - @mappings = {} - @devModeMappings = {} + @definitions = {} + @devModeDefinitions = {} + @activeElement = null + + @devModeDefinitions['#root-view'] = [{ type: 'separator' }] + @devModeDefinitions['#root-view'].push + label: 'Inspect Element' + command: 'application:inspect' + executeAtBuild: (e) -> + @.commandOptions = x: e.pageX, y: e.pageY # Public: Registers a command to be displayed when the relevant item is right # clicked. # # * selector: The css selector for the active element which should include # the given command in its context menu. - # * label: The text that should appear in the context menu. - # * command: The command string that should be triggered on the activeElement - # which matches your selector. + # * definition: The object containing keys which match the menu template API. # * options: # + devMode: Indicates whether this command should only appear while the # editor is in dev mode. - add: (selector, label, command, {devMode}={}) -> - mappings = if devMode then @devModeMappings else @mappings - mappings[selector] ?= [] - mappings[selector].push({label, command}) + add: (selector, definition, {devMode}={}) -> + definitions = if devMode then @devModeDefinitions else @definitions + (definitions[selector] ?= []).push(definition) - # Private: - bindingsForElement: (element, {devMode}={}) -> - mappings = if devMode then @devModeMappings else @mappings - items for selector, items of mappings when element.webkitMatchesSelector(selector) + # Private: Returns definitions which match the element and devMode. + definitionsForElement: (element, {devMode}={}) -> + definitions = if devMode then @devModeDefinitions else @definitions + matchedDefinitions = [] + for selector, items of definitions when element.webkitMatchesSelector(selector) + matchedDefinitions.push(_.clone(item)) for item in items - # Public: Used to generate the context menu for a specific element. + matchedDefinitions + + # Private: Used to generate the context menu for a specific element and it's + # parents. + # + # The menu items are sorted such that menu items that match closest to the + # active element are listed first. The further down the list you go, the higher + # up the ancestor hierarchy they match. # # * element: The DOM element to generate the menu template for. - menuTemplateForElement: (element) -> - menuTemplate = [] - for devMode in [false, true] - for items in @bindingsForElement(element, {devMode}) - for {label, command} in items - template = {label} - template.click = -> $(element).trigger(command) - menuTemplate.push(template) + menuTemplateForMostSpecificElement: (element, {devMode}={}) -> + menuTemplate = @definitionsForElement(element, {devMode}) + if element.parentElement + menuTemplate.concat(@menuTemplateForMostSpecificElement(element.parentElement, {devMode})) + else + menuTemplate - menuTemplate + # Private: Returns a menu template for both normal entries as well as + # development mode entries. + combinedMenuTemplateForElement: (element) -> + menuTemplate = @menuTemplateForMostSpecificElement(element) + menuTemplate.concat(@menuTemplateForMostSpecificElement(element, devMode: true)) + + # Private: Executes `executeAtBuild` if defined for each menu item with + # the provided event and then removes the `executeAtBuild` property from + # the menu item. + # + # This is useful for commands that need to provide data about the event + # to the command. + executeBuildHandlers: (event, menuTemplate) -> + for template in menuTemplate + template?.executeAtBuild?.call(template, event) + delete template.executeAtBuild + + # Public: Request a context menu to be displayed. + showForEvent: (event) -> + @activeElement = event.target + menuTemplate = @combinedMenuTemplateForElement(event.target) + @executeBuildHandlers(event, menuTemplate) + remote.getCurrentWindow().emit('context-menu', menuTemplate) diff --git a/src/context-menu.coffee b/src/context-menu.coffee index 3069bb3c4..f60a7709e 100644 --- a/src/context-menu.coffee +++ b/src/context-menu.coffee @@ -4,5 +4,17 @@ BrowserWindow = require 'browser-window' module.exports = class ContextMenu constructor: (template) -> - menu = Menu.buildFromTemplate template + template = @createClickHandlers(template) + menu = Menu.buildFromTemplate(template) menu.popup(BrowserWindow.getFocusedWindow()) + + # Private: It's necessary to build the event handlers in this process, otherwise + # closures are drug across processes and failed to be garbage collected + # appropriately. + createClickHandlers: (template) -> + for item in template + if item.command + (item.commandOptions ?= {}).contextCommand = true + item.click = do (item) -> + => global.atomApplication.sendCommand(item.command, item.commandOptions) + item diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 049b72548..9a5278d75 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -14,6 +14,9 @@ class WindowEventHandler @subscribe ipc, 'command', (command, args...) -> $(window).trigger(command, args...) + @subscribe ipc, 'context-command', (command, args...) -> + $(atom.contextMenu.activeElement).trigger(command, args...) + @subscribe $(window), 'focus', -> $("body").removeClass('is-blurred') @subscribe $(window), 'blur', -> $("body").addClass('is-blurred') @subscribe $(window), 'window:open-path', (event, {pathToOpen, initialLine}) -> @@ -39,14 +42,7 @@ class WindowEventHandler @subscribe $(document), 'contextmenu', (e) -> e.preventDefault() - menuTemplate = atom.contextMenu.menuTemplateForElement(e.target) - - # FIXME: This should be registered as a dev binding on - # atom.contextMenu, but I'm not sure where in the source. - menuTemplate.push({ type: 'separator' }) - menuTemplate.push({ label: 'Inspect Element', click: -> remote.getCurrentWindow().inspectElement(e.pageX, e.pageY) }) - - remote.getCurrentWindow().emit('context-menu', menuTemplate) + atom.contextMenu.showForEvent(e) openLink: (event) => location = $(event.target).attr('href') From c39ced381d201d65cd26c240885ae82c0fe3e072 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 19 Sep 2013 14:21:31 -0700 Subject: [PATCH 6/9] Implement declarative context menu loading --- docs/creating-a-package.md | 30 +++++ spec/atom-spec.coffee | 26 ++++ spec/context-menu-manager-spec.coffee | 121 ++++++++++++++++++ .../menus/menu-1.cson | 3 + .../menus/menu-2.cson | 3 + .../menus/menu-3.cson | 3 + .../package-with-menus-manifest/package.cson | 1 + .../package-with-menus/menus/menu-1.cson | 3 + .../package-with-menus/menus/menu-2.cson | 3 + .../package-with-menus/menus/menu-3.cson | 3 + src/atom-package.coffee | 14 ++ src/atom.coffee | 2 +- src/context-menu-manager.coffee | 24 +++- 13 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 spec/context-menu-manager-spec.coffee create mode 100644 spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.cson create mode 100644 spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.cson create mode 100644 spec/fixtures/packages/package-with-menus-manifest/menus/menu-3.cson create mode 100644 spec/fixtures/packages/package-with-menus-manifest/package.cson create mode 100644 spec/fixtures/packages/package-with-menus/menus/menu-1.cson create mode 100644 spec/fixtures/packages/package-with-menus/menus/menu-2.cson create mode 100644 spec/fixtures/packages/package-with-menus/menus/menu-3.cson diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md index 167a73008..be72a8ae2 100644 --- a/docs/creating-a-package.md +++ b/docs/creating-a-package.md @@ -42,6 +42,9 @@ directory are added alphabetically. - `keymaps`(**Optional**): an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the _keymaps_ directory are added alphabetically. +- `menus`(**Optional**): an Array of Strings identifying the order of +the menu mappings your package needs to load. If not specified, mappings +in the _keymap_ directory are added alphabetically. - `snippets` (**Optional**): an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the _snippets_ directory are added alphabetically. @@ -135,6 +138,33 @@ array in your _package.json_ can specify which keymaps to load and in what order See the [main keymaps documentation](../internals/keymaps.md) for more information on how keymaps work. +## Menus + +Menus are placed in the _menus_ subdirectory. It's useful to specify a +context menu items if if commands are linked to a specific part of the +interface, say for example adding a file in the tree-view. + +By default, all menus are loaded in alphabetical order. An optional +`menus` array in your _package.json_ can specify which menus to load +and in what order. + +Context menus are created by determining which element was selected and +then adding all of the menu items whose selectors match that element (in +the order which they were loaded). The process is then repeated for the +elements until reaching the top of the dom tree. + +NOTE: Currently you can only specify items to be added to the context +menu, the menu which appears when you right click. There are plans to +add support for adding to global menu. + +``` +'context-menu': + '.tree-view': + 'Add file': 'tree-view:add-file' + '#root-view': + 'Inspect Element': 'core:inspect' +``` + ## Snippets An extension can supply language snippets in the _snippets_ directory. These can diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 2139cccfb..52d7100b9 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -174,6 +174,32 @@ describe "the `atom` global", -> expect(keymap.bindingsForElement(element1)['ctrl-n']).toBe 'keymap-2' expect(keymap.bindingsForElement(element3)['ctrl-y']).toBeUndefined() + describe "menu loading", -> + describe "when the metadata does not contain a 'menus' manifest", -> + it "loads all the .cson/.json files in the keymaps directory", -> + element = ($$ -> @div class: 'test-1')[0] + + expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + + atom.activatePackage("package-with-menus") + + expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1" + expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3" + + describe "when the metadata contains a 'keymaps' manifest", -> + it "loads only the menus specified by the manifest, in the specified order", -> + element = ($$ -> @div class: 'test-1')[0] + + expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + + atom.activatePackage("package-with-menus-manifest") + + expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2" + expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1" + expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined() + + describe "stylesheet loading", -> describe "when the metadata contains a 'stylesheets' manifest", -> it "loads stylesheets from the stylesheets directory as specified by the manifest", -> diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee new file mode 100644 index 000000000..b819e7654 --- /dev/null +++ b/spec/context-menu-manager-spec.coffee @@ -0,0 +1,121 @@ +{$$} = require 'space-pen' + +ContextMenuManager = require 'context-menu-manager' + +describe "ContextMenuManager", -> + [contextMenu] = [] + + beforeEach -> + contextMenu = new ContextMenuManager + + describe "adding definitions", -> + it 'loads', -> + contextMenu.add 'file-path', + '.selector': + 'label': 'command' + + expect(contextMenu.definitions['.selector'][0].label).toEqual 'label' + expect(contextMenu.definitions['.selector'][0].command).toEqual 'command' + + describe 'dev mode', -> + it 'loads', -> + contextMenu.add 'file-path', + '.selector': + 'label': 'command' + , devMode: true + + expect(contextMenu.devModeDefinitions['.selector'][0].label).toEqual 'label' + expect(contextMenu.devModeDefinitions['.selector'][0].command).toEqual 'command' + + describe "building a menu template", -> + beforeEach -> + contextMenu.definitions = { + '.parent':[ + label: 'parent' + command: 'command-p' + ] + '.child': [ + label: 'child' + command: 'command-c' + ] + } + + contextMenu.devModeDefinitions = + '.parent': [ + label: 'dev-label' + command: 'dev-command' + ] + + describe "on a single element", -> + [element] = [] + + beforeEach -> + element = ($$ -> @div class: 'parent')[0] + + it "creates a menu with a single item", -> + menu = contextMenu.combinedMenuTemplateForElement(element) + + expect(menu[0].label).toEqual 'parent' + expect(menu[0].command).toEqual 'command-p' + expect(menu[1]).toBeUndefined() + + describe "in devMode", -> + beforeEach -> contextMenu.devMode = true + + it "creates a menu with development items", -> + menu = contextMenu.combinedMenuTemplateForElement(element) + + expect(menu[0].label).toEqual 'parent' + expect(menu[0].command).toEqual 'command-p' + expect(menu[1].label).toEqual 'dev-label' + expect(menu[1].command).toEqual 'dev-command' + + + describe "on multiple elements", -> + [element] = [] + + beforeEach -> + element = $$ -> + @div class: 'parent', => + @div class: 'child' + + element = element.find('.child')[0] + + it "creates a menu with a two items", -> + menu = contextMenu.combinedMenuTemplateForElement(element) + + expect(menu[0].label).toEqual 'child' + expect(menu[0].command).toEqual 'command-c' + expect(menu[1].label).toEqual 'parent' + expect(menu[1].command).toEqual 'command-p' + expect(menu[2]).toBeUndefined() + + describe "in devMode", -> + beforeEach -> contextMenu.devMode = true + + xit "creates a menu with development items", -> + menu = contextMenu.combinedMenuTemplateForElement(element) + + expect(menu[0].label).toEqual 'child' + expect(menu[0].command).toEqual 'command-c' + expect(menu[1].label).toEqual 'parent' + expect(menu[1].command).toEqual 'command-p' + expect(menu[2].label).toEqual 'dev-label' + expect(menu[2].command).toEqual 'dev-command' + expect(menu[3]).toBeUndefined() + + describe "#executeBuildHandlers", -> + menuTemplate = [ + label: 'label' + executeAtBuild: -> + ] + event = + target: null + + it 'should invoke the executeAtBuild fn', -> + buildFn = spyOn(menuTemplate[0], 'executeAtBuild') + contextMenu.executeBuildHandlers(event, menuTemplate) + + expect(buildFn).toHaveBeenCalled() + expect(buildFn.mostRecentCall.args[0]).toBe event + diff --git a/spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.cson b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.cson new file mode 100644 index 000000000..0e7e344cf --- /dev/null +++ b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 1": "command-1" diff --git a/spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.cson b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.cson new file mode 100644 index 000000000..415bcf74f --- /dev/null +++ b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 2": "command-2" diff --git a/spec/fixtures/packages/package-with-menus-manifest/menus/menu-3.cson b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-3.cson new file mode 100644 index 000000000..7296e4418 --- /dev/null +++ b/spec/fixtures/packages/package-with-menus-manifest/menus/menu-3.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 3": "command-3" diff --git a/spec/fixtures/packages/package-with-menus-manifest/package.cson b/spec/fixtures/packages/package-with-menus-manifest/package.cson new file mode 100644 index 000000000..1a3efbda3 --- /dev/null +++ b/spec/fixtures/packages/package-with-menus-manifest/package.cson @@ -0,0 +1 @@ +menus: ["menu-2", "menu-1"] diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-1.cson b/spec/fixtures/packages/package-with-menus/menus/menu-1.cson new file mode 100644 index 000000000..0e7e344cf --- /dev/null +++ b/spec/fixtures/packages/package-with-menus/menus/menu-1.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 1": "command-1" diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-2.cson b/spec/fixtures/packages/package-with-menus/menus/menu-2.cson new file mode 100644 index 000000000..415bcf74f --- /dev/null +++ b/spec/fixtures/packages/package-with-menus/menus/menu-2.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 2": "command-2" diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-3.cson b/spec/fixtures/packages/package-with-menus/menus/menu-3.cson new file mode 100644 index 000000000..7296e4418 --- /dev/null +++ b/spec/fixtures/packages/package-with-menus/menus/menu-3.cson @@ -0,0 +1,3 @@ +"context-menu": + ".test-1": + "Menu item 3": "command-3" diff --git a/src/atom-package.coffee b/src/atom-package.coffee index 338a4b623..97e7d7e30 100644 --- a/src/atom-package.coffee +++ b/src/atom-package.coffee @@ -17,6 +17,7 @@ class AtomPackage extends Package metadata: null keymaps: null + menus: null stylesheets: null grammars: null scopedProperties: null @@ -30,10 +31,12 @@ class AtomPackage extends Package if @isTheme() @stylesheets = [] @keymaps = [] + @menus = [] @grammars = [] @scopedProperties = [] else @loadKeymaps() + @loadMenus() @loadStylesheets() @loadGrammars() @loadScopedProperties() @@ -75,6 +78,7 @@ class AtomPackage extends Package activateResources: -> keymap.add(keymapPath, map) for [keymapPath, map] in @keymaps + atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus type = if @metadata.theme then 'theme' else 'bundled' applyStylesheet(stylesheetPath, content, type) for [stylesheetPath, content] in @stylesheets syntax.addGrammar(grammar) for grammar in @grammars @@ -84,6 +88,9 @@ class AtomPackage extends Package loadKeymaps: -> @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath)] + loadMenus: -> + @menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath)] + getKeymapPaths: -> keymapsDirPath = path.join(@path, 'keymaps') if @metadata.keymaps @@ -91,6 +98,13 @@ class AtomPackage extends Package else fsUtils.listSync(keymapsDirPath, ['cson', 'json']) + getMenuPaths: -> + menusDirPath = path.join(@path, 'menus') + if @metadata.menus + @metadata.menus.map (name) -> fsUtils.resolve(menusDirPath, name, ['json', 'cson', '']) + else + fsUtils.listSync(menusDirPath, ['cson', 'json']) + loadStylesheets: -> @stylesheets = @getStylesheetPaths().map (stylesheetPath) -> [stylesheetPath, loadStylesheet(stylesheetPath)] diff --git a/src/atom.coffee b/src/atom.coffee index 985d3662a..5432cb540 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -17,7 +17,7 @@ window.atom = activePackages: {} packageStates: {} themes: new ThemeManager() - contextMenu: new ContextMenuManager() + contextMenu: new ContextMenuManager(remote.getCurrentWindow().loadSettings.devMode) getLoadSettings: -> remote.getCurrentWindow().loadSettings diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 58f81014a..5b33a9e6c 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -9,7 +9,7 @@ remote = require 'remote' module.exports = class ContextMenuManager # Private: - constructor: -> + constructor: (@devMode=false) -> @definitions = {} @devModeDefinitions = {} @activeElement = null @@ -21,7 +21,22 @@ class ContextMenuManager executeAtBuild: (e) -> @.commandOptions = x: e.pageX, y: e.pageY - # Public: Registers a command to be displayed when the relevant item is right + # Public: Creates menu definitions from the object specified by the menu + # cson API. + # + # * name: The path of the file that contains the menu definitions. + # * object: The 'context-menu' object specified in the menu cson API. + # * options: + # + devMode - Determines whether the entries should only be shown when + # the window is in dev mode. + # + # Returns nothing. + add: (name, object, {devMode}={}) -> + for selector, items of object + for label, command of items + @addBySelector(selector, {label, command}, {devMode}) + + # Private: Registers a command to be displayed when the relevant item is right # clicked. # # * selector: The css selector for the active element which should include @@ -30,7 +45,7 @@ class ContextMenuManager # * options: # + devMode: Indicates whether this command should only appear while the # editor is in dev mode. - add: (selector, definition, {devMode}={}) -> + addBySelector: (selector, definition, {devMode}={}) -> definitions = if devMode then @devModeDefinitions else @definitions (definitions[selector] ?= []).push(definition) @@ -62,7 +77,8 @@ class ContextMenuManager # development mode entries. combinedMenuTemplateForElement: (element) -> menuTemplate = @menuTemplateForMostSpecificElement(element) - menuTemplate.concat(@menuTemplateForMostSpecificElement(element, devMode: true)) + menuTemplate = menuTemplate.concat(@menuTemplateForMostSpecificElement(element, devMode: true)) if @devMode + menuTemplate # Private: Executes `executeAtBuild` if defined for each menu item with # the provided event and then removes the `executeAtBuild` property from From f82449c8146ce701f0f5dc6905893b6f995bd1a7 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 19 Sep 2013 14:44:15 -0700 Subject: [PATCH 7/9] Only show context menu separator when it makes sense. --- src/context-menu-manager.coffee | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 5b33a9e6c..1c9d540d1 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -14,12 +14,12 @@ class ContextMenuManager @devModeDefinitions = {} @activeElement = null - @devModeDefinitions['#root-view'] = [{ type: 'separator' }] - @devModeDefinitions['#root-view'].push + @devModeDefinitions['#root-view'] = [ label: 'Inspect Element' command: 'application:inspect' executeAtBuild: (e) -> @.commandOptions = x: e.pageX, y: e.pageY + ] # Public: Creates menu definitions from the object specified by the menu # cson API. @@ -76,9 +76,12 @@ class ContextMenuManager # Private: Returns a menu template for both normal entries as well as # development mode entries. combinedMenuTemplateForElement: (element) -> - menuTemplate = @menuTemplateForMostSpecificElement(element) - menuTemplate = menuTemplate.concat(@menuTemplateForMostSpecificElement(element, devMode: true)) if @devMode - menuTemplate + normalItems = @menuTemplateForMostSpecificElement(element) + devItems = if @devMode then @menuTemplateForMostSpecificElement(element, devMode: true) else [] + + menuTemplate = normalItems + menuTemplate.push({ type: 'separator' }) if normalItems.length > 0 and devItems.length > 0 + menuTemplate.concat(devItems) # Private: Executes `executeAtBuild` if defined for each menu item with # the provided event and then removes the `executeAtBuild` property from From e8125defb94ade2b5220e440249c91323e97dc01 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Thu, 19 Sep 2013 14:55:17 -0700 Subject: [PATCH 8/9] Update specs to match latest changes --- spec/atom-spec.coffee | 6 ++++-- spec/context-menu-manager-spec.coffee | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 52d7100b9..a61da1c85 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -175,8 +175,10 @@ describe "the `atom` global", -> expect(keymap.bindingsForElement(element3)['ctrl-y']).toBeUndefined() describe "menu loading", -> + beforeEach -> atom.contextMenu.definitions = [] + describe "when the metadata does not contain a 'menus' manifest", -> - it "loads all the .cson/.json files in the keymaps directory", -> + it "loads all the .cson/.json files in the menus directory", -> element = ($$ -> @div class: 'test-1')[0] expect(atom.contextMenu.definitionsForElement(element)).toEqual [] @@ -187,7 +189,7 @@ describe "the `atom` global", -> expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2" expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3" - describe "when the metadata contains a 'keymaps' manifest", -> + describe "when the metadata contains a 'menus' manifest", -> it "loads only the menus specified by the manifest, in the specified order", -> element = ($$ -> @div class: 'test-1')[0] diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index b819e7654..f21553027 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -67,8 +67,9 @@ describe "ContextMenuManager", -> expect(menu[0].label).toEqual 'parent' expect(menu[0].command).toEqual 'command-p' - expect(menu[1].label).toEqual 'dev-label' - expect(menu[1].command).toEqual 'dev-command' + expect(menu[1].type).toEqual 'separator' + expect(menu[2].label).toEqual 'dev-label' + expect(menu[2].command).toEqual 'dev-command' describe "on multiple elements", -> From ca3f6453014181482cd0bc168601dbdd27e6f31b Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Fri, 20 Sep 2013 14:14:17 -0700 Subject: [PATCH 9/9] Fix requires --- spec/context-menu-manager-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index f21553027..42c833bda 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -1,6 +1,6 @@ -{$$} = require 'space-pen' +{$$} = require 'atom' -ContextMenuManager = require 'context-menu-manager' +ContextMenuManager = require '../src/context-menu-manager' describe "ContextMenuManager", -> [contextMenu] = []