Code covered by the BSD License  

Highlights from
EditorMacro - assign a macro to a keyboard key-stroke in the Matlab Editor and Command Window

image thumbnail

EditorMacro - assign a macro to a keyboard key-stroke in the Matlab Editor and Command Window

by

 

01 Jul 2009 (Updated )

EditorMacro assigns a macro or action to requested key-binding in the Matlab Editor & Command Window

EditorMacro(keystroke, macro, macroType)
function [bindingsList, actionsList] = EditorMacro(keystroke, macro, macroType)
% EditorMacro assigns a macro to a keyboard key-stroke in the Matlab-editor
%
% Syntax:
%    [bindingsList, actionsList] = EditorMacro(keystroke, macro, macroType)
%    [bindingsList, actionsList] = EditorMacro(bindingsList)
%
% Description:
%    EditorMacro assigns the specified MACRO to the requested keyboard
%    KEYSTROKE, within the context of the Matlab Editor and Command Window.
%
%    KEYSTROKE is a string representation of the keyboard combination.
%    Special modifiers (Alt, Ctrl or Control, Shift, Meta, AltGraph) are
%    recognized and should be separated with a space, dash (-), plus (+) or
%    comma (,). At least one of the modifiers should be specified, otherwise
%    very weird things will happen...
%    For a full list of supported keystrokes, see:
%    <a href="http://tinyurl.com/2s63e9">http://java.sun.com/javase/6/docs/api/java/awt/event/KeyEvent.html</a>
%    If KEYSTROKE was already defined, then it will be updated (overridden).
%
%    MACRO should be in one of Matlab's standard callback formats: 'string',
%    @FunctionHandle or {@FunctionHandle,arg1,...}, or any of several hundred
%    built-in editor action-names - read MACROTYPE below for a full
%    description. To remove a KEYSTROKE-MACRO definition, simply enter an
%    empty MACRO ([], {} or '').
%
%    MACROTYPE is an optional input argument specifying the type of action
%    that MACRO is expected to do:
%
%     - 'text' (=default value) indicates that if the MACRO is a:
%        1. 'string': this string will be inserted as-is into the current
%             editor caret position (or replace the selected editor text).
%             Multi-line strings can be set using embedded \n's (example: 
%             'Multi-line\nStrings'). This can be used to insert generic
%             comments or code templates (example: 'try \n catch \n end').
%        2. @FunctionHandle -  the specified function will be invoked with
%             two input arguments: the editorPane object and the eventData
%             object (the KEYSTROKE event details). FunctionHandle is
%             expected to return a string which will then be inserted into
%             the editor document as expained above.
%        3. {@FunctionHandle,arg1,...} - like #2, but the function will be
%             called with the specified arg1+ as input args #3+, following
%             the editorPane and eventData args.
%
%     - 'run' indicates that MACRO should be invoked as a Matlab command,
%             just like any regular Matlab callback. The accepted MACRO
%             formats and function input args are exactly like for 'text'
%             above, except that no output string is expected and no text
%             insertion/replacement will be done (unless specifically done
%             within the invoked MACRO command/function). This MACROTYPE is
%             useful for closing/opening files, moving to another document
%             position and any other non-textual action.
%
%             In addition, this MACROTYPE accepts all available (built-in)
%             editor action names. Valid action names can be listed by
%             requesting the ACTIONSLIST output argument.
%
%    BINDINGSLIST = EditorMacro returns the list of currently-defined
%    KEYSTROKE bindings as a 4-columned cell array: {keystroke, macro, type,
%    class}. The class information indicates a built-in action ('editor menu
%    action', 'editor native action', 'cmdwin native action' or 'cmdwin menu
%    action') or a user-defined action ('text' or 'user-defined macro').
%
%    BINDINGSLIST = EditorMacro(KEYSTROKE) returns the bindings list for
%    the specified KEYSTROKE as a 4-columned cell array: {keystroke, macro,
%    type, class}.
%
%    BINDINGSLIST = EditorMacro(KEYSTROKE,MACRO) returns the bindings list
%    after defining a specific KEYSTROKE-MACRO binding.
%
%    EditorMacro(BINDINGSLIST) can be used to set a bunch of key bindings
%    using a single command. BINDINGSLIST is the cell array returned from
%    a previous invocation of EditorMacro, or by manual construction (just
%    be careful to set the keystroke strings correctly!). Only non-native
%    bindings are updated in this bulk mode of operation.
%
%    [BINDINGSLIST, ACTIONSLIST] = EditorMacro(...) returns in ACTIONSLIST a
%    3-columned cell array of all available built-in actions and currently-
%    associated key-biding(s): {actionName, keyBinding(s), class}.
%
% Examples:
%    bindingsList = EditorMacro;  % get list of current key-bindings
%    bindingsList = EditorMacro('ctrl r');  % get list of bindings for <Ctrl>-R
%    [bindings,actions] = EditorMacro;  % get list of available built-in action-names
%    EditorMacro('Ctrl Shift C', '%%% Main comment %%%\n% \n% \n% \n');
%    EditorMacro('Alt-x', 'try\n  % Main code here\ncatch\n  % Exception handling here\nend');
%    EditorMacro('Ctrl-Alt C', @myCallbackFunction);  % myCallbackFunction returns a string to insert
%    EditorMacro('Alt control t', @(a,b)datestr(now), 'text');  % insert current timestamp
%    EditorMacro('Shift-Control d', {@computeDiameter,3.14159}, 'run');
%    EditorMacro('Alt L', 'to-lower-case', 'run') % Built-in action: convert text to lowercase
%    EditorMacro('ctrl D','open-selection','run') % Override default Command-Window action (=delete)
%                                                 % to behave as in the Editor (=open selected file)
%
% Known limitations (=TODO for future versions):
%    1. Multi-keystroke bindings (e.g., 'alt-U,L') are not supported
%    2. In Matlab 6, macro insertions are un-undo-able (ok in Matlab 7)
%    3. Key bindings are sometimes lost when switching between a one-document
%       editor and a two-document one (i.e., adding/closing the second doc)
%    4. Key bindings are not saved between editor sessions
%    5. In split-pane mode, when inserting a macro on the secondary (right/
%       bottom) pane, then both panes (and the actual document) are updated
%       but the secondary pane does not display the inserted macro (the
%       primary pane looks ok).
%    6. Native menu/editor/command-window actions cannot be updated in bulk
%       mode (EditorMacro(BINDINGSLIST)) - only one at a time.
%    7. Key-bindings may be fogotten when switching between docked/undocked
%       editor or between the Command-Window and other docked desktop panes
%       (such as Command History, Profiler etc.)
%    8. In Matlab 6, actions are not supported: only user-defined text/macro
%
% Bugs and suggestions:
%    Please send to Yair Altman (altmany at gmail dot com)
%
% Warning:
%    This code heavily relies on undocumented and unsupported Matlab
%    functionality. It works on Matlab 6 & 7+, but use at your own risk!
%
%    A technical description of the implementation can be found at:
%    <a href="http://undocumentedmatlab.com/blog/EditorMacro/">http://UndocumentedMatlab.com/blog/EditorMacro/</a>
%
% Change log:
%    2011-01-31: Fixes for R2011a
%    2009-10-26: Fixes for Matlab 6
%    2009-08-19: Support for command-window actions; use EDT for text replacement
%    2009-08-11: Several fixes; support for native/menu actions (idea by Perttu Ranta-aho)
%    2009-07-03: Fixed Matlab 6 edge-case; Automatically detect macro functions that do not accept the expected two input args
%    2009-07-01: First version posted on <a href="http://www.mathworks.com/matlabcentral/fileexchange/authors/27420">MathWorks File Exchange</a>

% Programmed by Yair M. Altman: altmany(at)gmail.com
% $Revision: 1.6 $  $Date: 2011/01/31 20:47:25 $

  persistent appdata
  try
      % Check input args
      if nargin == 2
          macroType = 'text';
      elseif nargin == 3 & ~ischar(macroType)  %#ok Matlab 6 compatibility
          myError('YMA:EditorMacro:badMacroType','MacroType must be ''text'', ''run'' or ''native''');
      elseif nargin > 3
          myError('YMA:EditorMacro:tooManyArgs','too many input argument');
      end

      % Try to get the editor's Java handle
      jEditor = getJEditor;

      % ...and the editor's main documents container handle
      jMainPane = getJMainPane(jEditor);
      hEditorPane = getEditorPane(jMainPane.getComponent(0));  % first document

      % ...and the desktop's Command Window handle
      try
          jCmdWin = [];
          %mde = jMainPane.getRootPane.getParent.getDesktop;
          mde = com.mathworks.mde.desk.MLDesktop.getInstance;  % different in ML6!!!
          hCmdWin = mde.getClient('Command Window');
          jCmdWin = hCmdWin.getComponent(0).getComponent(0).getComponent(0);  % com.mathworks.mde.cmdwin.XCmdWndView
          jCmdWin = handle(jCmdWin, 'CallbackProperties');
      catch
          % Maybe ML6 or... - never mind...
      end

      % Now try to get the persistent list of macros
      try
          appdata = getappdata(jEditor,'EditorMacro');
      catch
          % never mind - we will use the persistent object instead...
      end

      % If no EditorMacro has been set yet
      if isempty(appdata)
          % Set the initial binding hashtable
          appdata = {}; %java.util.Hashtable is better but can't extract {@function}...

          % Add editor native action bindings to the bindingsList
          edActionsMap = getAccelerators(hEditorPane); % =getAccelerators(hEditorPane.getActiveTextComponent);
          appdata.bindings = getBindings({},edActionsMap,'editor native action');

          % Add CW native action bindings to the bindingsList
          cwActionsMap = getAccelerators(jCmdWin);
          appdata.bindings = getBindings(appdata.bindings,cwActionsMap,'cmdwin native action');

          % Add editor menu action bindings to the bindingsList
          appdata.edMenusMap = getMenuShortcuts(jMainPane);
          appdata.bindings = getBindings(appdata.bindings,appdata.edMenusMap,'editor menu action');

          % Add CW menu action bindings to the bindingsList
          appdata.cwMenusMap = getMenuShortcuts(jCmdWin);
          appdata.bindings = getBindings(appdata.bindings,appdata.cwMenusMap,'cmdwin menu action');

          % Loop over all the editor's currently-open documents
          for docIdx = 1 : jMainPane.getComponentCount
              % Instrument these documents to catch user keystrokes
              instrumentDocument([],[],jMainPane.getComponent(docIdx-1),jEditor,appdata);
          end

          % Update the editor's ComponentAdded callback to also instrument new documents
          set(jMainPane,'ComponentAddedCallback',{@instrumentDocument,[],jEditor,appdata})
          if isempty(get(jMainPane,'ComponentAddedCallback'))
              pause(0.1);
              set(jMainPane,'ComponentAddedCallback',{@instrumentDocument,[],jEditor,appdata})
          end

          % Also instrument the CW to catch user keystrokes
          set(jCmdWin, 'KeyPressedCallback', {@keyPressedCallback,jEditor,appdata,jCmdWin});
      end

      % If any macro setting is requested
      if nargin
          % Update the bindings list with the new key binding
          if nargin > 1
              appdata = updateBindings(appdata,keystroke,macro,macroType,jMainPane,jCmdWin);
              setappdata(jEditor,'EditorMacro',appdata);
          elseif iscell(keystroke) & (isempty(keystroke) | size(keystroke,2)==4) %#ok Matlab 6 compatibility
              appdata = keystroke;  % passed updated bindingsList as input arg
              setappdata(jEditor,'EditorMacro',appdata);
          elseif ischar(keystroke) | isa(keystroke,'javax.swing.KeyStroke')  %#ok Matlab 6 compatibility
              setappdata(jEditor,'EditorMacro',appdata);
              keystroke = normalizeKeyStroke(keystroke);
              bindingIdx = strmatch(keystroke,appdata.bindings(:,1),'exact');
              appdata.bindings = appdata.bindings(bindingIdx,:);  % only return matching bindings
          else
              myError('YMA:EditorMacro:invalidBindingsList','invalid BINDINGSLIST input argument');
          end
      end

      % Check if output is requested
      if nargout
          if ~iscell(appdata.bindings)
              appdata.bindings = {};
          end
          bindingsList = appdata.bindings;

          % Return the available actionsList
          if nargout > 1
              % Start with the native editor actions
              actionsList = listActionsMap(hEditorPane);
              try [actionsList{:,3}] = deal('editor native action'); catch, end

              % ...add the desktop's CW native actions...
              cwActionsList = listActionsMap(jCmdWin);
              try [cwActionsList{:,3}] = deal('cmdwin native action'); catch, end
              actionsList = [actionsList; cwActionsList];

              % ...and finally add the menu actions
              try
                  menusList = appdata.edMenusMap(:,[2,1]);  % {action,keystroke}
                  try [menusList{:,3}] = deal('editor menu action'); catch, end
                  actionsList = [actionsList; menusList];
              catch
                  % never mind...
              end

              try
                  menusList = appdata.cwMenusMap(:,[2,1]);
                  try [menusList{:,3}] = deal('cmdwin menu action'); catch, end
                  actionsList = [actionsList; menusList];
              catch
                  % never mind...
              end
          end
      end

  % Error handling
  catch
      handleError;
  end

%% Get the current list of key-bindings
function bindings = getBindings(bindings,actionsMap,groupName)
  try
      for bindingIdx = 1 : size(actionsMap,1)
          ks = actionsMap{bindingIdx,1};
          if ~isempty(ks)
              actionName = actionsMap{bindingIdx,2};
              [bindings(end+1,:)] = {ks,actionName,'run',groupName};  %#ok grow
          end
      end
  catch
      % never mind...
  end

%% Modify native shortcuts
function bindingsList = nativeShortcuts(keystroke, macro, jMainPane)
  % Note: this function is unused
  hEditorPane = getEditorPane(jMainPane.getComponent(0));  % first document
  switch lower(keystroke(1:5))
      case 'listn'
          % List all active accelerators
          bindingsList = getAccelerators(hEditorPane.getActiveTextComponent);
          if ~nargout
              for ii = 1 : size(bindingsList,1)
                  disp(sprintf('%-35s %s',bindingsList{ii,:}))
              end
          end

      case 'lista'
          % List all available actions
          bindingsList = listActionsMap(hEditorPane,nargout);

      otherwise
          % Bind native action
          bindingsList = getNativeActions(hEditorPane);
          if ~ismember(macro,bindingsList) & ~isempty(macro)  %#ok Matlab 6 compatibility
              myError('YMA:EditorMacro:invalidNativeAction','invalid Native Action');
          end
          [keystroke,jKeystroke] = normalizeKeyStroke(keystroke);
          %jkeystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);
          for docIdx = 1 : jMainPane.getComponentCount
              % Instrument these documents to catch user keystrokes
              hEditorPane = getEditorPane(jMainPane.getComponent(docIdx-1));
              inputMap = hEditorPane.getInputMap;
              removeShortcut(inputMap,jKeystroke);
              if ~isempty(macro)
                  action = hEditorPane.getActionMap.get(macro);
                  inputMap.put(jKeystroke,action);
              end
          end
          removeMenuShortcut(jMainPane,keystroke);
  end

%% Get/list all available key-bindings for all the possible actions
function bindingsList = listActionsMap(hEditorPane,nargoutFlag)
  try
      bindingsList  = getNativeActions(hEditorPane);
      try
          % Editor accelerators are stored in the activeTextComponent
          accellerators = getAccelerators(hEditorPane.getActiveTextComponent);
      catch
          % The CW doesn't have an activeTextComponent...
          accellerators = getAccelerators(hEditorPane);
      end
      for ii = 1 : size(bindingsList,1)
          actionKeys = accellerators(strcmpi(accellerators(:,2),bindingsList{ii}),1);
          if numel(actionKeys)==1
              actionKeys = actionKeys{1};
              keysStr = actionKeys;
          elseif isempty(actionKeys)
              actionKeys = [];
              keysStr = '  [not assigned]';
          else  % multiple keys assigned
              keysStr = strcat(char(actionKeys),',')';
              keysStr = keysStr(:)';  % =reshape(keysStr,1,numel(keysStr));
              keysStr = strtrim(strrep(keysStr,',',', '));
              keysStr = regexprep(keysStr,'\s+',' ');
              keysStr = keysStr(1:end-1);  % remove trailing ','
          end
          bindingsList{ii,2} = actionKeys;
          if nargin > 1 & ~nargoutFlag  %#ok Matlab 6 compatibility
              %disp(bindingsList)
              disp(sprintf('%-35s %s',bindingsList{ii,1},keysStr))
          end
      end
  catch
      % never mind...
  end

%% Get all available actions (even those without any key-binding)
function actionNames = getNativeActions(hEditorPane)
  try
      actionNames = {};
      actionKeys = hEditorPane.getActionMap.allKeys;
      actionNames = cellfun(@char,cell(actionKeys),'UniformOutput',false);
      actionNames = sort(actionNames);
  catch
      % never mind...
  end

%% Get all active native shortcuts
function accelerators = getAccelerators(hEditorPane)
  try
      accelerators = cell(0,2);
      inputMap = hEditorPane.getInputMap;
      inputKeys = inputMap.allKeys;
      accelerators = cell(numel(inputKeys),2);
      for ii = 1 : numel(inputKeys)
          accelerators(ii,:) = {char(inputKeys(ii)), char(inputMap.get(inputKeys(ii)))};
      end
      accelerators = sortrows(accelerators,1);
  catch
      % never mind...
  end

%% Remove shortcut from inputMap
function removeShortcut(inputMap,keystroke)
    if ~isa(keystroke,'javax.swing.KeyStroke')
        keystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);
    end
    inputMap.remove(keystroke);
    %keys = inputMap.allKeys;
    %for ii = 1:length(keys)
    %    if keys(ii) == keystroke
    %        % inputMap.remove(keystroke);
    %        inputMap.put(keystroke,[]); %,null(1));
    %        return
    %    end
    %end
    try
        % keystroke not found - try to find it in the parent inputMap...
        removeShortcut(inputMap.getParent,keystroke)
    catch
        % Never mind
    end

%% Get the list of all menu actions
function menusMap = getMenuShortcuts(jMainPane)
    try
        menusMap = {};
        jRootPane = jMainPane.getRootPane;
        jMenubar = jRootPane.getMenuBar; %=jRootPane.getComponent(1).getComponent(1);
        for menuIdx = 1 : jMenubar.getMenuCount
            % top-level menus should be treated differently than sub-menus
            jMenu = jMenubar.getMenu(menuIdx-1);
            menusMap = getMenuShortcuts_recursive(jMenu,menusMap);
        end
    catch
        % Never mind
    end

%% Recursively get the list of all menu actions
function menusMap = getMenuShortcuts_recursive(jMenu,menusMap)
    try
        numMenuComponents = getNumMenuComponents(jMenu);
        for child = 1 : numMenuComponents
            menusMap = getMenuShortcuts_recursive(jMenu.getMenuComponent(child-1),menusMap);
        end
    catch
        % Probably a simple menu item, not a sub-menu - add it to the menusMap
        try
            accelerator = char(jMenu.getAccelerator);
            if isempty(accelerator)
                accelerator = [];  % ''=>[]
            end
            [menusMap(end+1,:)] = {accelerator, char(jMenu.getActionCommand), jMenu};
        catch
            % maybe a separator or something strange... - ignore
        end
    end

%% Remove shortcut from Menu items inputMap
function menusMap = removeMenuShortcut(menusMap,keystroke)
    try
        keystroke = normalizeKeyStroke(keystroke);
        map = cellfun(@char,menusMap(:,1),'un',0);
        oldBindingIdx = strmatch(keystroke,map,'exact');
        if ~isempty(oldBindingIdx)
            menusMap{oldBindingIdx,1} = '';
            menusMap{oldBindingIdx,3}.setAccelerator([]);
        end
    catch
        % never mind...
    end

%% Remove shortcut from Menu items inputMap
function removeMenuShortcut_old(jMainPane,keystroke)
    % Note: this function was replaced by removeMenuShortcut()
    try
        % Try to remove any corresponding menu-item accelerator
        jRootPane = jMainPane.getRootPane;
        jMenubar = jRootPane.getMenuBar; %=jRootPane.getComponent(1).getComponent(1);
        if ~isa(keystroke,'javax.swing.KeyStroke')
            keystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);
        end
        for menuIdx = 1 : jMenubar.getMenuCount
            % top-level menus should be treated differently than sub-menus
            jMenu = jMenubar.getMenu(menuIdx-1);
            if removeMenuShortcut_recursive(jMenu,keystroke)
                return;
            end
        end
    catch
        % never mind...
    end

%% Recursively remove shortcut from Menu items inputMap
function found = removeMenuShortcut_recursive(jMenu,keystroke)
    try
        % Try to remove any corresponding menu-item accelerator
        found = 0;
        if ~isempty(jMenu.getActionForKeyStroke(keystroke))
            jMenu.setAccelerator([]);
            found = 1;
            return;
        end

        % Not found - try to dig further
        try
            numMenuComponents = getNumMenuComponents(jMenu);
            for child = 1 : numMenuComponents
                found = removeMenuShortcut_recursive(jMenu.getMenuComponent(child-1),keystroke);
                if found
                    return;
                end
            end
        catch
            % probably a simple menu item, not a sub-menu - ignore
            %a=1;  % debuggable breakpoint...
        end
    catch
        % never mind...
    end

%% Get the number of menu sub-elements
function numMenuComponents  = getNumMenuComponents(jcontainer)

    % The following line will raise an Exception for anything except menus
    numMenuComponents = jcontainer.getMenuComponentCount;

    % No error so far, so this must be a menu container...
    % Note: Menu subitems are not visible until the top-level (root) menu gets initial focus...
    % Try several alternatives, until we get a non-empty menu (or not...)
    % TODO: Improve performance - this takes WAY too long...
    if jcontainer.isTopLevelMenu & (numMenuComponents==0)
        jcontainer.requestFocus;
        numMenuComponents = jcontainer.getMenuComponentCount;
        if (numMenuComponents == 0)
            drawnow; pause(0.001);
            numMenuComponents = jcontainer.getMenuComponentCount;
            if (numMenuComponents == 0)
                jcontainer.setSelected(true);
                numMenuComponents = jcontainer.getMenuComponentCount;
                if (numMenuComponents == 0)
                    drawnow; pause(0.001);
                    numMenuComponents = jcontainer.getMenuComponentCount;
                    if (numMenuComponents == 0)
                        jcontainer.doClick;  % needed in order to populate the sub-menu components
                        numMenuComponents = jcontainer.getMenuComponentCount;
                        if (numMenuComponents == 0)
                            drawnow; %pause(0.001);
                            numMenuComponents = jcontainer.getMenuComponentCount;
                            jcontainer.doClick;  % close menu by re-clicking...
                            if (numMenuComponents == 0)
                                drawnow; %pause(0.001);
                                numMenuComponents = jcontainer.getMenuComponentCount;
                            end
                        else
                            % ok - found sub-items
                            % Note: no need to close menu since this will be done when focus moves to another window
                            %jcontainer.doClick;  % close menu by re-clicking...
                        end
                    end
                end
                jcontainer.setSelected(false);  % de-select the menu
            end
        end
    end

%% Get the Java editor component handle
function jEditor = getJEditor
  jEditor = [];
  try
      % Matlab 7
      jEditor = com.mathworks.mde.desk.MLDesktop.getInstance.getGroupContainer('Editor');
  catch
      % Matlab 6
      try
          %desktop = com.mathworks.ide.desktop.MLDesktop.getMLDesktop; % no use - can't get to the editor from here...
          openDocs = com.mathworks.ide.editor.EditorApplication.getOpenDocuments;  % a java.util.Vector
          firstDoc = openDocs.elementAt(0);  % a com.mathworks.ide.editor.EditorViewContainer object
          jEditor = firstDoc.getParent.getParent.getParent;  % a com.mathworks.mwt.MWTabPanel or com.mathworks.ide.desktop.DTContainer object
      catch
          myError('YMA:EditorMacro:noEditor','Cannot retrieve the Matlab editor handle - possibly no open editor');
      end
  end
  if isempty(jEditor)
      myError('YMA:EditorMacro:noEditor','Cannot retrieve the Matlab editor handle - possibly no open editor');
  end
  try
      jEditor = handle(jEditor,'CallbackProperties');
  catch
      % never mind - might be Matlab 6...
  end

%% Get the Java editor's main documents container handle
function jMainPane = getJMainPane(jEditor)
  jMainPane = [];
  try
      v = version;
      if (v(1) >= '7')
          for childIdx = 1 : jEditor.getComponentCount
              componentClassName = regexprep(jEditor.getComponent(childIdx-1).class,'.*\.','');
              if any(strcmp(componentClassName,{'DTMaximizedPane','DTFloatingPane','DTTiledPane'}))
                  jMainPane = jEditor.getComponent(childIdx-1);
                  break;
              end
          end
          if isa(jMainPane,'com.mathworks.mwswing.desk.DTFloatingPane')
              jMainPane = jMainPane.getComponent(0);  % a com.mathworks.mwswing.desk.DTFloatingPane$2 object
          end
      else
          for childIdx = 1 : jEditor.getComponentCount
              if isa(jEditor.getComponent(childIdx-1),'com.mathworks.mwt.MWGroupbox') | ... 
                 isa(jEditor.getComponent(childIdx-1),'com.mathworks.ide.desktop.DTClientFrame') %#ok Matlab 6 compatibility
                  jMainPane = jEditor.getComponent(childIdx-1);
                  break;
              end
          end
      end
  catch
      % Matlab 6 - ignore for now...
  end
  if isempty(jMainPane)
      myError('YMA:EditorMacro:noMainPane','Cannot find the Matlab editor''s main document pane');
  end
  try
      jMainPane = handle(jMainPane,'CallbackProperties');
  catch
      % never mind - might be Matlab 6...
  end

%% Get EditorPane
function hEditorPane = getEditorPane(jDocPane)
  try
      % Matlab 7   TODO: good for ML 7.1-7.7: need to check other versions
      jSyntaxTextPaneView = getDescendent(jDocPane,[0,0,1,0,0,0,0]);
      if isa(jSyntaxTextPaneView,'com.mathworks.widgets.SyntaxTextPaneMultiView$1')
          hEditorPane(1) = handle(getDescendent(jSyntaxTextPaneView.getComponent(1),[1,0,0]),'CallbackProperties');
          hEditorPane(2) = handle(getDescendent(jSyntaxTextPaneView.getComponent(2),[1,0,0]),'CallbackProperties');
      else
          jEditorPane = getDescendent(jSyntaxTextPaneView,[1,0,0]);
          hEditorPane = handle(jEditorPane,'CallbackProperties');
      end
  catch
      % Matlab 6
      hEditorPane = getDescendent(jDocPane,[0,0,0,0]);
      if isa(hEditorPane,'com.mathworks.mwt.MWButton')  % edge case
          hEditorPane = getDescendent(jDocPane,[0,1,0,0]);
      end
  end

%% Internal error processing
function myError(id,msg)
  v = version;
  if (v(1) >= '7')
      error(id,msg);
  else
      % Old Matlab versions do not have the error(id,msg) syntax...
      error(msg);
  end

%% Error handling routine
function handleError
      v = version;
      if v(1)<='6'
          err.message = lasterr;  %#ok no lasterror function...
      else
          err = lasterror;  %#ok
      end
      try
          err.message = regexprep(err.message,'Error .* ==> [^\n]+\n','');
      catch
          try
              % Another approach, used in Matlab 6 (where regexprep is unavailable)
              startIdx = findstr(err.message,'Error using ==> ');
              stopIdx = findstr(err.message,char(10));
              for idx = length(startIdx) : -1 : 1
                  idx2 = min(find(stopIdx > startIdx(idx)));  %#ok ML6
                  err.message(startIdx(idx):stopIdx(idx2)) = [];
              end
          catch
              % never mind...
          end
      end
      if isempty(findstr(mfilename,err.message))
          % Indicate error origin, if not already stated within the error message
          err.message = [mfilename ': ' err.message];
      end
      if v(1)<='6'
          while err.message(end)==char(10)
              err.message(end) = [];  % strip excessive Matlab 6 newlines
          end
          error(err.message);
      else
          rethrow(err);
      end

%% Main keystroke callback function
function instrumentDocument(jObject,jEventData,jDocPane,jEditor,appdata)  %#ok jObject is unused
  try
      if isempty(jDocPane)
          % This happens when we get here via the jEditor's ComponentAddedCallback
          % (when adding a new document pane)
          try
              % Matlab 7
              jDocPane = jEventData.getChild;
          catch
              % Matlab 6
              eventData = get(jObject,'ComponentAddedCallbackData');
              jDocPane = eventData.child;
          end
      end
      hEditorPane = getEditorPane(jDocPane);

      % Note: KeyTypedCallback is called less frequently (i.e. better),
      % ^^^^  but unfortunately it does not catch alt/ctrl combinations...
      %set(hEditorPane, 'KeyTypedCallback', {@keyPressedCallback,jEditor,appdata,hEditorPane});
      set(hEditorPane, 'KeyPressedCallback', {@keyPressedCallback,jEditor,appdata,hEditorPane});
      pause(0.01);  % needed in Matlab 6...
  catch
      % never mind - might be Matlab 6...
  end

%% Recursively get the specified children
function child = getDescendent(child,listOfChildrenIdx)
  if ~isempty(listOfChildrenIdx)
      child = getDescendent(child.getComponent(listOfChildrenIdx(1)),listOfChildrenIdx(2:end));
  end

%% Update the bindings list
function appdata = updateBindings(appdata,keystroke,macro,macroType,jMainPane,jCmdWin)

  [keystroke,jKeystroke] = normalizeKeyStroke(keystroke);
  %jKeystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);
  %appdata.put(keystroke,macro);  %using java.util.Hashtable is better but can't extract {@function}...
  try
      oldBindingIdx = strmatch(keystroke,appdata.bindings(:,1),'exact');
      appdata.bindings(oldBindingIdx,:) = [];  % clear any possible old binding
  catch
      % ignore - possibly empty appdata
      a=1;  % debug point
  end

  % Remove native key-bindings from all editor documents / menu-items
  try
      for docIdx = 1 : jMainPane.getComponentCount
          hEditorPane = getEditorPane(jMainPane.getComponent(docIdx-1));
          inputMap = hEditorPane.getInputMap;
          removeShortcut(inputMap,keystroke);
      end
      appdata.edMenusMap = removeMenuShortcut(appdata.edMenusMap,keystroke);
      appdata.cwMenusMap = removeMenuShortcut(appdata.cwMenusMap,keystroke);
  catch
      % never mind...
  end

  try
      if ~isempty(macro)

          % Normalize the requested macro (if it's a string): '\n', ...
          if ischar(macro)
              macro = sprintf(strrep(macro,'%','%%'));
          end

          % Check & normalize the requested macroType
          if ~ischar(macroType)
              myError('YMA:EditorMacro:badMacroType','bad MACROTYPE input argument - must be a ''string''');
          elseif isempty(macroType) | ~any(lower(macroType(1))=='rt')  %#ok for Matlab6 compatibility
              myError('YMA:EditorMacro:badMacroType','bad MACROTYPE input argument - must be ''text'' or ''run''');
          elseif lower(macroType(1)) == 'r'
              macroType = 'run';
              macroClass = 'user-defined macro';
          else
              macroType = 'text';
              macroClass = 'text';
          end

          % Check if specified macro is a native and/or menu action name
          if ischar(macro) & macroType(1)=='r'  %#ok Matlab 6 compatibility

              % Check for editor native action name
              actionFound = 0;
              hEditorPane = getEditorPane(jMainPane.getComponent(0));  % first document
              actionNames = getNativeActions(hEditorPane);
              if any(strcmpi(macro,actionNames))  %#ok Matlab 6 compatibility
                  % Specified macro appears to be a valid native action name
                  for docIdx = 1 : jMainPane.getComponentCount
                      % Add requested action binding to all editor documents' inputMaps
                      hEditorPane = getEditorPane(jMainPane.getComponent(docIdx-1));
                      inputMap = hEditorPane.getInputMap;
                      %removeShortcut(inputMap,jKeystroke);
                      action = hEditorPane.getActionMap.get(macro);
                      inputMap.put(jKeystroke,action);
                  end
                  [appdata.bindings(end+1,:)] = {keystroke,macro,macroType,'editor native action'};
                  actionFound = 1;
              end

              % Check for CW native action name
              actionNames = getNativeActions(jCmdWin);
              if any(strcmpi(macro,actionNames))  %#ok Matlab 6 compatibility
                  % Specified macro appears to be a valid native action name
                  % Add requested action binding to the CW inputMap
                  inputMap = jCmdWin.getInputMap;
                  %removeShortcut(inputMap,jKeystroke);
                  action = jCmdWin.getActionMap.get(macro);
                  inputMap.put(jKeystroke,action);
                  [appdata.bindings(end+1,:)] = {keystroke,macro,macroType,'cmdwin native action'};
                  actionFound = 1;
              end

              % Check for editor menu action name
              oldBindingIdx = find(strcmpi(macro, appdata.bindings(:,2)) & ...
                                   strcmpi('editor menu action', appdata.bindings(:,4)));
              appdata.bindings(oldBindingIdx,:) = [];  %#ok clear any possible old binding
              menuItemIdx = find(strcmpi(macro,appdata.edMenusMap(:,2)));
              for menuIdx = 1 : length(menuItemIdx)
                  appdata.edMenusMap{menuItemIdx(menuIdx),1} = keystroke;
                  appdata.edMenusMap{menuItemIdx(menuIdx),3}.setAccelerator(jKeystroke);
                  [appdata.bindings(end+1,:)] = {keystroke,macro,macroType,'editor menu action'};
                  actionFound = 1;
              end

              % Check for CW menu action name
              oldBindingIdx = find(strcmpi(macro, appdata.bindings(:,2)) & ...
                                   strcmpi('cmdwin menu action', appdata.bindings(:,4)));
              appdata.bindings(oldBindingIdx,:) = [];  %#ok clear any possible old binding
              menuItemIdx = find(strcmpi(macro,appdata.cwMenusMap(:,2)));
              for menuIdx = 1 : length(menuItemIdx)
                  appdata.cwMenusMap{menuItemIdx(menuIdx),1} = keystroke;
                  appdata.cwMenusMap{menuItemIdx(menuIdx),3}.setAccelerator(jKeystroke);
                  [appdata.bindings(end+1,:)] = {keystroke,macro,macroType,'cmdwin menu action'};
                  actionFound = 1;
              end

              % Bail out if native and/or menu action was handled
              if actionFound
                  return;
              end
          end

          % A non-native macro - Store the new/updated key-binding in the bindings list
          [appdata.bindings(end+1,:)] = {keystroke,macro,macroType,macroClass};
      end
  catch
      myError('YMA:EditorMacro:badMacro','bad MACRO or MACROTYPE input argument - read the help section');
  end

%% Normalize the keystroke string to a standard format
function [keystroke,jKeystroke] = normalizeKeyStroke(keystroke)
  try
      if ~ischar(keystroke)
          myError('YMA:EditorMacro:badKeystroke','bad KEYSTROKE input argument - must be a ''string''');
      end
      keystroke = strrep(keystroke,',',' ');  % ',' => space (extra spaces are allowed)
      keystroke = strrep(keystroke,'-',' ');  % '-' => space (extra spaces are allowed)
      keystroke = strrep(keystroke,'+',' ');  % '+' => space (extra spaces are allowed)
      [flippedKeyChar,flippedMods] = strtok(fliplr(keystroke));
      keyChar   = upper(fliplr(flippedKeyChar));
      modifiers = lower(fliplr(flippedMods));

      keystroke = sprintf('%s %s', modifiers, keyChar);  % PRESSED: the character needs to be UPPERCASE, all modifiers lowercase
      %keystroke = sprintf('%s typed %s',   modifiers, keyChar);  % TYPED: in runtime, the callback is for Typed, not Pressed

      jKeystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);  % normalize & check format validity
      keystroke = char(jKeystroke.toString);  % javax.swing.KeyStroke => Matlab string
      %keystroke = strrep(keystroke, 'pressed', 'released');  % in runtime, the callback is for Typed, not Pressed
      %keystroke = strrep(keystroke, '-P', '-R');  % a different format in Matlab 6 (=Java 1.1.8)...
      keystroke = strrep(keystroke,'keyCode ','');  % Fix for JVM 1.1.8 (Matlab 6)
      if isempty(keystroke)
          myError('YMA:EditorMacro:badKeystroke','bad KEYSTROKE input argument');
      end
  catch
      myError('YMA:EditorMacro:badKeystroke','bad KEYSTROKE input argument - see help section');
  end

%% Main keystroke callback function
function keyPressedCallback(jEditorPane,jEventData,jEditor,appdata,hEditorPane,varargin)
  try
      try
          appdata = getappdata(jEditor,'EditorMacro');
      catch
          % gettappdata() might fail on Matlab 6 so it will fallback to the supplied appdata input arg
      end

      % Normalize keystroke string
      try
          keystroke = javax.swing.KeyStroke.getKeyStrokeForEvent(jEventData);
          %get(jEventData)
      catch
          % Matlab 6 - for some reason, all Fn keys don't work with KeyReleasedCallback, but some of them work ok with KeyPressedCallback...
          jEventData = get(jEditorPane,'KeyPressedCallbackData');
          keystroke = javax.swing.KeyStroke.getKeyStroke(jEventData.keyCode, jEventData.modifiers);
          keystroke = char(keystroke.toString);  % no automatic type-casting in Matlab 6...
          keystroke = strrep(keystroke,'keyCode ','');  % Fix for JVM 1.1.8 (Matlab 6)
          jEditorPane = hEditorPane;  % bypass Matlab 6 quirk...
      end

      % If this keystroke was bound to a macro
      macroIdx = strmatch(keystroke,appdata.bindings(:,1),'exact');
      if ~isempty(macroIdx)

          % Disregard built-in actions - they are dispatched via a separate mechanism
          userTextIdx  = strcmp(appdata.bindings(macroIdx,4),'text');
          userMacroIdx = strcmp(appdata.bindings(macroIdx,4),'user-defined macro');
          if ~any(userTextIdx) & ~any(userMacroIdx)  %#ok Matlab 6 compatibility
              return;
          end

          % Dispatch the defined macro
          macro = appdata.bindings{macroIdx,2};
          macroType = appdata.bindings{macroIdx,3};

          switch lower(macroType(1))

              case 't'  % Text
                  if ischar(macro)
                      % Simple string - insert as-is
                  elseif iscell(macro)
                      % Cell or cell array - feval this cell
                      macro = myFeval(macro{1}, jEditorPane, jEventData, macro{2:end});
                  else  % assume @FunctionHandle
                      % feval this @FunctionHandle
                      macro = myFeval(macro, jEditorPane, jEventData);
                  end

                  % Now insert the resulting string into the jEditorPane caret position or replace selection
                  %caretPosition = jEditorPane.getCaretPosition;
                  try
                      % Matlab 7
                      %jEditorPane.insert(caretPosition, macro);  % better to use replaceSelection() than insert()
                      try
                          % Try to dispatch on EDT
                          awtinvoke(jEditorPane, 'replaceSelection', macro);
                      catch
                          try
                              awtinvoke(java(jEditorPane), 'replaceSelection', macro);
                          catch
                              % no good - try direct invocation
                              jEditorPane.replaceSelection(macro);
                          end
                      end
                  catch
                      % Matlab 6
                      %jEditorPane.insert(macro, caretPosition);  % note the reverse order of input args vs. Matlab 7...
                      try
                          % Try to dispatch on EDT
                          awtinvoke(jEditorPane, 'replaceRange', macro, jEditorPane.getSelStart, jEditorPane.getSelEnd);
                      catch
                          try
                              awtinvoke(java(jEditorPane), 'replaceRange', macro, jEditorPane.getSelStart, jEditorPane.getSelEnd);
                          catch
                              % no good - try direct invocation
                              jEditorPane.replaceRange(macro, jEditorPane.getSelStart, jEditorPane.getSelEnd);
                          end
                      end
                  end
                  
              case 'r'  % Run
                  if ischar(macro)
                      % Simple string - evaluate in the base
                      evalin('base', macro);
                  elseif iscell(macro)
                      % Cell or cell array - feval this cell
                      myFeval(macro{1}, jEditorPane, jEventData, macro{2:end});
                  else  % assume @FunctionHandle
                      % feval this @FunctionHandle
                      myFeval(macro, jEditorPane, jEventData);
                  end
          end
      end
  catch
      % never mind... - ignore error
      %lasterr
      try err = lasterror;  catch, end  %#ok for debugging, will fail on ML6
      dummy=1;          %#ok debugging point
  end

%% Evaluate a function with exception handling to automatically fix too-many-inputs problems
function result = myFeval(func,varargin)
  try
      result = [];
      if nargout
          result = feval(func,varargin{:});
      else
          feval(func,varargin{:});
      end
  catch
      % Try rerunning the function without the two default args
      %v = version;
      %if v(1)<='6'
      %    err.identifier = 'MATLAB:TooManyInputs';  %#ok no lasterror function so assume...
      %else
      %    err = lasterror;  %#ok
      %end
      %if strcmpi(err.identifier,'MATLAB:TooManyInputs')
          if nargout
              result = feval(func,varargin{3:end});
          else
              feval(func,varargin{3:end});
          end
      %end
  end
  

%{ 
% TODO:
% =====
% - Handle Multi-KeyStroke bindings as in Alt-U U (Text / Uppercase)
% - Support native actions in EditorMacro(BINDINGSLIST) bulk mode of operation
% - Fix docking/menu limitations
% - GUI interface (list of actions, list of keybindings
%}

Contact us