- Undocumented Matlab - https://undocumentedmatlab.com -

EditorMacro v2 – setting Command Window key-bindings

Some weeks ago, I introduced my EditorMacro utility [1] as a means of assigning user-defined text strings and runnable macros (callbacks) to keyboard shortcuts in the Matlab Editor. I later showed how EditorMacro can be used to set non-textual editor macros [2].
Since then, I was contacted by several users with questions, enhancement requests and improvement suggestions. Of these, the most notable was from Perttu Ranta-aho who suggested (and provided sample code for) using the editor’s built-in actions. The next logical step was to extend this to support the editor menus, and then extend again to support Command Window key-bindings. We bounced several increasingly-powerful versions of the utility between us, until arriving at the version I uploaded yesterday, which has the following significant improvements over the first version:

Built-in native actions

It turns out that the Editor and Command-Window both have some ~200 built-in (native) available actions, about half of them common. Actions are identified by name, which is a lowercase dash-separated description like ‘selection-up’ (this format is familiar to Emacs users). Altogether, there are over 300 unique built-in actions which can be used, but only ~100 of them are assigned a default key-bindings. A few dozen actions have multiple assigned key-bindings. For example, the ‘selection-up’ action is assigned to both ‘shift pressed UP’ (=<shift>-<up>) and ‘shift pressed KP_UP’ (=<shift>-<Keypad-up>):

>> [bindings, actions] = EditorMacro
actions =
...[snip]
 'selection-page-down'     'shift pressed PAGE_DOWN'     'editor native action'
 'selection-page-up'       'shift pressed PAGE_UP'       'editor native action'
 'selection-previous-word'                   {2x1 cell}  'editor native action'
 'selection-up'                              {2x1 cell}  'editor native action'
 'set-read-only'                                     []  'editor native action'
 'set-writable'                                      []  'editor native action'
 'shift-insert-break'      'shift pressed ENTER'         'editor native action'
 'shift-line-left'         'ctrl pressed OPEN_BRACKET'   'editor native action'
 'shift-line-right'        'ctrl pressed CLOSE_BRACKET'  'editor native action'
 'shift-tab-pressed'       'shift pressed TAB'           'editor native action'
...[snip]...
 'toggle-typing-mode'      'pressed INSERT'              'editor native action'
 'uncomment'               'ctrl pressed T'              'editor native action'
 'undo'                    'ctrl pressed Z'              'editor native action'
 'unselect'                'pressed ESCAPE'              'editor native action'
 'adjust-window-bottom'                              []  'cmdwin native action'
 'adjust-window-top'                                 []  'cmdwin native action'
 'beep'                    'ctrl pressed G'              'cmdwin native action'
 'break-interrupt'         'ctrl pressed CANCEL'         'cmdwin native action'
...[snip]...
 'toggle-typing-mode'      'pressed INSERT'              'cmdwin native action'
 'unselect'                'ctrl pressed BACK_SLASH'     'cmdwin native action'
 'new-mfile'               'ctrl pressed N'              'editor menu action'
 'Figure'                                            []  'editor menu action'
...[snip]...

Even more interesting, apparently some 200 actions do not have any pre-assigned default key-bindings, such as ‘set-read-only’ and ‘set-writable’ in the snippet above. Let’s take the ‘match-brace’ action for example. This sounded promising so I assigned it an unused key-binding and indeed found that it can be very useful: if your cursor is placed on a beginning or end of some code, clicking the assigned key-binding will jump the cursor to the other end, and then back again. This works nicely for (..), [..], for..end, try..end, if..end, etc.

>> % Ensure that -M is unassigned
>> bindings = EditorMacro('alt m')
bindings =
   Empty cell array: 0-by-4
>> % Assign the key-binding and verify
>> EditorMacro('alt m','match-brace','run');
>> bindings = EditorMacro('alt m')
b =
  'alt pressed M'  'match-brace'  'run'  'editor native action'

Implementation details

Interested readers of this post are encouraged to look within EditorMacro’s source code [3] and see how the native actions and keybindings were retrieved and modified for the editor, Command-Window and menus. In a nutshell, the native action names and key-bindings are stored in a Java Map object. Here’s a code snippet:

%% 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),'Uniform',0);
    actionNames = sort(actionNames);
  catch
    % never mind...
  end
%% Get all active native shortcuts (key-bindings)
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)
      thisKey = inputKeys(ii);
      thisAction = inputMap.get(thisKey);
      accelerators(ii,:) = {char(thisKey), char(thisAction)};
    end
    accelerators = sortrows(accelerators,1);
  catch
    % never mind...
  end

The menu retrieval was more difficult: while it is possible to directly access the menubar reference (jMainPane.getRootPane.getMenuBar), the menu items themselves are not visible until their main menu item is clicked (displayed). The only way I know to access menu actions/keybindings is to read them from the individual menu items (if anyone knows a better way please tell me – perhaps some central key-listener repository?). Therefore, a simulation of the menu-click events is done and the menu hierarchy is traveled recuresively to collect all its actions and key-bindings.
A final note relates to the use of EDT. EDT really deserves a separate post, but in a nutshell it means that any action that affects the GUI needs to be invoked asynchronously (via the EDT) rather than synchronously (on the main Matlab thread). This is no real problem in the editor, but it is indeed an issue in the Command Window: If we do not use EDT there, we get ugly red stack-trace exceptions thrown on the Command Window whenever we run our EditorMacro-assigned macro. Here’s the code snippet that solves this:

try
   % Matlab 7
   %jEditorPane.insert(caretPosition, macro);  % better to use replaceSelection() than insert()
   try
      % Try to dispatch on EDT
      awtinvoke(jEditorPane.java, 'replaceSelection', macro);
   catch
      % no good - try direct invocation
      jEditorPane.replaceSelection(macro);
   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.java, 'replaceRange', macro, jEditorPane.getSelStart, jEditorPane.getSelEnd);
   catch
      % no good - try direct invocation
      jEditorPane.replaceRange(macro, jEditorPane.getSelStart, jEditorPane.getSelEnd);
   end
end

Known limitations

Some limitations remain in EditorMacro – here are the major ones:

The first couple of limitations have a non-perfect workaround that Perttu came up with. He implemented this in his KeyBindings utility [4] on the Matlab File Exchange. Perhaps one day Perttu or me will find the time to merge these utilities into one.
Have you designed some crafty user-defined macro? or found some important unassigned built-in action? Please share your experience using EditorMacro in the comments section below.

4 Comments (Open | Close)

4 Comments To "EditorMacro v2 – setting Command Window key-bindings"

#1 Comment By jo On August 21, 2009 @ 13:43

Thanks!
EditorMacro has been added to the startup.m file! 🙂
So far I’ve created few macros that remarkably speed up my work.
One of them is a macro that executes code between tags embedded in a comment of an m-file – excellent alternative for Edit Run Configuration… + F5 combination during debugging an m-file, but… I cannot solve one problem: automatic saving file before run (eg. to activate fresh breakpoints).
Your post about internal commands gave me new hope. I’ve made some trials to save m-file programmatically but without effect.

Do you know if such an action is possible from inside the macro?

#2 Comment By Yair Altman On August 22, 2009 @ 12:47

@Jo – Here’s a snippet of a sample macro (callback) function:

function myCallback(jDocument, jEventData, varargin)
   filename = jDocument.getFilename;
   % do some user-defined stuff

   % Now save the file:
   com.mathworks.mlservices.MLEditorServices.saveDocument(filename);
end  % myCallback

Also look at Perttu’s KeyBindings utility for other usage examples of MLEditorServices.

Yair

#3 Comment By mb On January 13, 2012 @ 05:38

Hi,
In Matlab 2011 com.mathworks.mlservices.MLEditorServices has no such method as saveDocument.
Instead of it one can use matlab editor api to save the document:

filehandle = matlab.desktop.editor.getActive 
filehandle.save() ;

More about it can be found here [11] .
I hope it will save someone’s time.

Regards,
mb

#4 Comment By Yair Altman On January 13, 2012 @ 05:59

This is indeed a very good example of the risks in using internal Matlab components – their API can change dramatically from one release to another, without prior notice.

Here is the alternative in the latest Matlab release (12a):

% Get the reference to the editor application
jEditorApp = com.mathworks.mde.editor.MatlabEditorApplication.getInstance;
jEditorApp = com.mathworks.mlservices.MLEditorServices.getEditorApplication;  % an alternative

% Save the current file
jEditorApp.getActiveEditor.save();
jEditorApp.getActiveEditor.saveAs(filename);

Other useful methods of the jEditorApp.getActiveEditor reference are:

addEventListener(com.mathworks.matlab.api.editor.EditorEventListener)
addPropertyChangeListener(java.beans.PropertyChangeListener)
addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
appendText(java.lang.String)
bringToFront()
close()
closeNoPrompt()
dispose()
fireEditorEvent(com.mathworks.matlab.api.editor.EditorEvent)
firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object)
getBreakpointMargin() : com.mathworks.matlab.api.debug.BreakpointMargin
getCaretPosition() : int
getComponent() : java.awt.Component
getDocument() : javax.swing.text.Document
getExecutionArrowMargin() : com.mathworks.matlab.api.debug.ExecutionArrowMargin
getLanguage() : com.mathworks.matlab.api.editor.EditorLanguage
getLength() : int
getLineNumber() : int
getLongName() : java.lang.String
getProperty(java.lang.Object) : java.lang.Object
getSelection() : java.lang.String
getShortName() : java.lang.String
getStorageLocation() : com.mathworks.matlab.api.datamodel.StorageLocation
getText() : java.lang.String
getTextComponent() : javax.swing.text.JTextComponent
getTextWithSystemLineEndings() : java.lang.String
goToFunction(java.lang.String, java.lang.String)
goToLine(int, boolean)
goToLine(int, int)
goToPositionAndHighlight(int, int)
insertAndFormatTextAtCaret(java.lang.String)
insertTextAtCaret(java.lang.String)
isBuffer() : boolean
isDirty() : boolean
isEditable() : boolean
isMCode() : boolean
isOpen() : boolean
lineAndColumnToPosition(int, int) : int
lockIfOpen() : boolean
negotiateSave() : boolean
positionToLineAndColumn(int) : int[]
putProperty(java.lang.String, java.lang.Object)
refreshMenus()
reload()
reloadAndReturnError() : java.lang.String
removeEventListener(com.mathworks.matlab.api.editor.EditorEventListener)
removePropertyChangeListener(java.beans.PropertyChangeListener)
removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
replaceText(java.lang.String, int, int)
save()
saveAndReturnError() : java.lang.String
saveAs(java.lang.String)
saveAsAndReturnError(java.lang.String) : java.lang.String
setCaretPosition(int)
setClean()
setDirtyUntilSave()
setEditable(boolean)
setEditorStatusBarText(java.lang.String)
setSelection(int, int)
setStatusText(java.lang.String)
smartIndentContents()
unlock()