Over the past years there have been quite a few requests to enable keyboard macros and keybinding modifications in the Matlab editor. Some posters have even noted this lack as their main reason to use an external editor. Coincidentally, some weeks ago I was approached by a reader (Grant Roch) to help with assigning some textual editor macros, and with joint work we were able to figure out a basic working mechanism. The latest user comment on this issue was posted on the official Matlab Desktop blog yesterday. All this prompted me to finally post a solution to this need: My EditorMacro utility is now available for download on the Mathworks File Exchange. In this post I will detail some of its inner workings, which rely on undocumented Matlab features.
In a nutshell, EditorMacro sets the KeyPressedCallback property (explained in a previous post) for each of the editor’s document panes, to an internal function. This internal function then checks each keystroke against a list of registered keybindings. The list itself is persisted in the editor object’s hidden ApplicationData property (accessible via the getappdata/setappdata built-in functions). If a match is found, then the associated macro is invoked. Depending on the macro type, some text can be inserted at the current editor caret position, or replacing the selected editor text, or a non-text Matlab function/command can be invoked.
This enables EditorMacro to be used for quickly inserting code templates (header comments, try-catch blocks etc.) or for automating Matlab unit testing.
Here’s a typical usage example: start by defining a simple function that returns a dynamic header comment:
function comment = createHeaderComment(hDocument, eventData) timestamp = datestr(now); username = getenv('username'); %computer = getenv('computername'); % unused lineStr = repmat('%',1,35); comment = sprintf(... ['%s\n' ... '%% \n' ... '%% Name: functionName\n' ... '%% \n' ... '%% Desc: enter description here\n' ... '%% \n' ... '%% Inputs: enter inputs here\n' ... '%% \n' ... '%% Outputs: enter outputs here\n' ... '%% \n' ... '%% Created: %s\n' ... '%% \n' ... '%% Author: %s\n' ... '%% \n' ... '%s\n'], ... lineStr, timestamp, username, lineStr); end % createHeaderComment |
Now define a macro to use this function, and another simple try-catch template macro:
>> EditorMacro('Alt-Control-h', @createHeaderComment); >> macroStr = 'try\n % Main code here\ncatch\n % Exception handling here\nend'; >> b = EditorMacro('Ctrl alt T', macroStr) b = 'ctrl alt pressed H' @createHeaderComment 'text' 'ctrl alt pressed T' [1x60 char] 'text' |
Now start with a blank document and click <Ctrl>-<Alt>-H and <Ctrl>-<Alt>-T. This is the end result:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Name: functionName % % Desc: enter description here % % Inputs: enter inputs here % % Outputs: enter outputs here % % Created: 01-Jul-2009 23:31:46 % % Author: Yair Altman % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% try % Main code here catch % Exception handling here end
Keybindings are normalized using Java’s built-in javax.swing.KeyStroke.getKeyStroke() method, to enable the user a very wide range of keystroke naming formats (e.g., ‘Alt-Control-T’ or ‘ctrl alt t’).
I have taken great pains to make EditorMacro compatible with all Matlab versions since 6.0 (R12). This was no easy feat: Matlab 7 made some significant changes to the editor layout. Luckily, once I found out how to get a handle to the Matlab 6 editor object (this took some hours of trial-and-error), listing its display hierarchy was simple and the modifications were generally straight-forward, although non-trivial (different quirks due to missing default type-castings, missing eventData in invoked callbacks etc…). Please feel free to look at EditorMacro‘s source code for the clearly-marked Matlab 6 segments.
For the record, here is a snippet showing how to get the editor object in Matlab 6 and 7. Note that the desktop handle is only useful to get the editor handle in Matlab 6 – we need a totally different way in Matlab 6:
try % Matlab 7 jDesktop = com.mathworks.mde.desk.MLDesktop.getInstance; jEditor = jDesktop.getGroupContainer('Editor'); catch % Matlab 6 try %desktop = com.mathworks.ide.desktop.MLDesktop.getMLDesktop; % no use: can't get to the editor from the desktop handle in Matlab 6... openDocs = com.mathworks.ide.editor.EditorApplication.getOpenDocuments; firstDoc = openDocs.elementAt(0); jEditor = firstDoc.getParent.getParent.getParent; catch error('Cannot find Matlab editor handle: possibly no open documents'); end end |
Another complication arose due to the different layout used for floating/maximized/tiled document layout in the editor. Yet another was due to the different behavior (at least on Matlab 6) between a one-document and a multi-document editor view.
Due to the way keyboard events are processed by the Matlab editor, KeyPressedCallback needs to be set separately for all the open document panes and split-panes. Since we also wish newly-opened documents to recognize the macro bindings, we need to set the common container ancestor’s ComponentAddedCallback to an internal function that will handle the KeyPressedCallback instrumentation for each newly-opened document. Again, this needs to be done somewhat differently for Matlab 6/7.
Note that EditorMacro relies on the editor’s internal display layout, which is very sensitive to modification between Matlab releases (as it has between Matlab 6 and 7, for example).
All-in-all, I believe EditorMacro provides a useful and long-awaited service, which should in fact be part of the built-in editor.
Is there a reason that you use a try-catch statement to differentiate between matlab 6 and 7 as opposed to
@Jason – that’s a good question. There are several reasons I use try-catch extensively:
1. Performance – try-catch is much faster than programmatic logic testing. This is true in general, and is not specific to Matlab (i.e., it also holds true for Java, C++, …). This is because at the machine-code level, try instrumentation translates into a single trap/wait (or equivalent) instrumentation opcode taking microsecs, whereas actual programmatic testing may take millisecs or longer. This may not seem much, but it adds up, especially within loops or recursive functions.
2. Correctness – the code difference may not split nicely along the Matlab 6/7 boundary – perhaps some aspects from Matlab 6 made it into Matlab 7.0 and were only changed in Matlab 7.1? using try-catch solves this.
3. Simplicity – try-catch is a lot simpler to code and less likely to contain bugs than programmatic logic.
4. Debugging – one can never think of everything in advance. Using copious try-catch blocks and placing breakpoints in the catch sections has yielded so many surprising bugs for me that I’ve lost count. Instead of trying to figure out in advance all the different logic tests, I simply place a try-catch block and let mother nature take its course…
I believe that getting used to writing code with copious try-catch blocks will give payback sooner rather than later. For me it is now second nature.
Yair
Great post — one step closer into the black hole void of the matlab gui. Do you know of any way of running commands other than inserting text, for example, moving the cursor, deleting a word/line, or more dynamic text editing?
n
@Neil – In answer to your question, I posted a follow-up: http://undocumentedmatlab.com/blog/non-textual-editor-actions/
Removing text can be done using hDocument.delete(startPos,endPos)
Enjoy 🙂
Yair
[…] The EditorMacro utility was extended to support built-in Matlab Editor and Command-Window actions and key-bindings. This post describes the changes and the implementation details. […]
Yair Hi!
There are also some reasons why using try-catch isn’t good in this case
1) Simple Code – The code is harder to understand.
2) Debugging – When using try-catch without taking a look at the caught exception, you might miss an error which isn’t related to Matlab version at all. Since you catch it, you will never see the stack in Matlab.
3) Case handling – You will eventually need another try-catch for the matlab 6 section, to handle other versions (not 6 or 7). So what you get is basically a lot of recursive try-catch, which makes the code even less understandable.
Andrey – you are correct: try-catch should be used, but excessive use has drawbacks. I guess this is true for most things in life… As elsewhere, there isn’t a clear-cut definition of how much is “just right” – we should all apply judgment and common-sense.
RE: the try/catch case, the verLessThan function is meant to address providing version-dependent behavior.
..forgot to mention that to @Jason that str2double is dangerous b/c some versions of MATLAB such as R14SP1 don’t have straightfoward numbers, it’s 7.0.1
Unfortunately Mike, varLessThan was only introduced in recent Matlab releases, so if I have an older release I would get an error. Which brings me back to try/catch which works for all Matlab releases 🙂
True. It’s on the file exchange for that purpose, but I understand. Just wanted to point it out, though.
in any version specific code I’ve written, I had actually accounted for the 7.0.1 issue by taking only the first 3 characters max, which would give me 7.0 or 7.5 etc. I had just heavily simplified for posting. so really line 2 should read: