From 037d39e943a3373170b2a1f583b94b600288fa04 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Wed, 18 Sep 2013 09:53:57 -0700 Subject: [PATCH] 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')