Code covered by the BSD License  

Highlights from
KeyBindings - Alter Matlab Editor key bindings

image thumbnail
from KeyBindings - Alter Matlab Editor key bindings by Perttu Ranta-aho
KeyBindings alters Matlab Editor's keyboard shortcuts.

bindingsList=KeyBindings(keystroke, actionID, context)
function bindingsList=KeyBindings(keystroke, actionID, context)
% KEYBINDINGS - Alter Matlab Editor key bindings
%
% Syntax:
%    bindingsList = KeyBindings(keystroke, actionID)
%
% Description:
%    KeyBindings alters Matlab Editor's keyboard shortcuts. 
%
%    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 (-) or plus (+).
%    Multi-key shortcuts can be created by separating keystrokes with 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).
%
%    In addition to java keystroke representation the keystroke can be one
%    of the following code words:
%        Save - Store current key bindings to file under $prefdir.
%        Load - Read previously saved key bindings from file.
%
%    ACTIONID should be name of one of the Matlab's build-in actions.
%    Available actions with their descriptions and current key bindings
%    are listed when no input arguments are given. 
%
%    After any modification to key bindings all open documents are reloaded
%    to update shortcuts. Thus all documents have to be saved before
%    modifying key bindings.
%                
% Note:
%    Saving and reading key bindings to file works only in R2009a.
%    Otherwise this should work with R2007b+. 
%    
%    It is not possible to assing keystrokes to text templates or custom
%    Matlab functions, only built-in actions are supported. 
%
%
% Examples:
%    % List current key bindings
%    KeyBindings
%    % Add new key binding
%    KeyBindings('Shift Ctrl M','show-mlint-report')
%    % Alter multiple key bindings at once
%    KeyBindings({'Ctrl B,B','Ctrl B,N','Ctrl B,P'},...
%                {'toggle-bookmark','next-bookmark','prev-bookmark'})
%    % Remove keybinding
%    KeyBindings('Ctrl-R','')
%
%
% Warning:
%    This code heavily relies on undocumented and unsupported Matlab
%    functionality. Use at your own risk!
%
%
% Acknowledgements:
%    This functions borrows code and ideas from EditorMacro
%    http://www.mathworks.com/matlabcentral/fileexchange/24615. 
%
%
% Known issues (TODO):
%  - Document reloading sometimes suffless document ordering.
%  - It is not possible to specify several shortcuts to one action.
%
% Bugs and suggestions:
%    Please send to Perttu Ranta-aho (rantaaho at gmail dot com)

% The "undocumented" features
%    Third input argument CONTEXT refers to Keybindings context. Valid
%    values depend on Matlab version, they can be listed with
%    KeyBindings('Contexts'). Context values are case sensitive. Currently
%    only 'MATLABEditor' context works, I don't know a way to refresh
%    keyboard shortcuts of other elements.

% Perttu Ranta-aho, <rantaaho (at) gmail dot com>
% $Revision: 1.3 $ $Date: 2009/08/20 13:02:20 $

if nargin < 2
    if nargin == 0
        keystroke = 'MATLABEditor';
    end
    % Save/Load keybindings
    switch lower(keystroke(1:4))
        case 'load',
            if anyModifiedDocs
                myError('PRa:KeyBindings:ModifiedDocuments',...
                    'Save all your documents before modifying Editor key bindings.')
            end
            readBindingsFromFile;
            reloadDocuments;
            return
        case 'save',
            writeBindingsToFile;
            return
        case 'cont',
            listContexts;
            return
        otherwise
            if isValidContext(keystroke)
                bindingsList = getShortcuts(keystroke);
            else
                myError('PRa:KeyBindings:invalidContext',...
                    'Invalid context argument.');
            end
    end
    if nargout == 0
        prettyPrint(bindingsList)
        clear bindingsList
    end
    return
elseif nargin > 3,
    myError('PRa:KeyBindings:tooManyInputs','Too many input arguments.');
end

% Check Context
if nargin < 3,
    context = 'MATLABEditor';
elseif ~isValidContext(context)
    myError('PRa:KeyBindings:invalidContext',...
        'Invalid context argument. See help section for valid values.');
end
if strcmp(context,'MATLABEditor') && anyModifiedDocs
    myError('PRa:KeyBindings:ModifiedDocuments',...
            'Save all your documents before modifying Editor key bindings.')
end
bindingsList = getShortcuts(context);

% Ensure that keystoke and actionID are cell arrays
if ~iscell(keystroke)
    keystroke = {keystroke};
end
if ~iscell(actionID)
    actionID = {actionID};
end
if numel(keystroke) ~= numel(actionID),
    myError('PRa:KeyBindings:sizeMismatch',...
        'Size mismatch in Keystroke Cell / Action Cell pair');
end

% Check that given actionIDs are all valid
editorActionList = cellfun(@(x)x{1},bindingsList,'UniformOutput',false);
editorActionList{end+1} = '';
validActions = ismember(actionID,editorActionList);
if ~all(validActions)
    invalidActions = actionID(~validActions);
    myError('PRa:KeyBindings:invalidActionID',...
        ['Invalid actionID(s): ', ...
        sprintf('%s ',invalidActions{:})]);
end

% Prepare new shortcut sequences
newShortcuts = cell(numel(keystroke),2);
for ii = 1:numel(keystroke)
    keyStrokeList = [];
    keystrokes = textscan(keystroke{ii},'%s','Delimiter',',');
    for k=keystrokes{1}'
        shortcut = normalizeKeyStroke(k{1});
        if isempty(keyStrokeList)
            keyStrokeList = [javax.swing.KeyStroke.getKeyStroke(shortcut) []];
        else
            keyStrokeList(end+1) = javax.swing.KeyStroke.getKeyStroke(shortcut); %#ok<AGROW>
        end
    end
    newShortcuts(ii,:) = {actionID{ii},keyStrokeList};
end
% Bind new shortcuts
bindShortcuts(newShortcuts,context);
% FIXME: How to refresh other components?
if strcmpi(context,'MATLABEditor')
    reloadDocuments;
end
% Don't return bindingsList unless asked.
if nargout>0
    bindingsList = getShortcuts(context); % Refresh
else
    clear bindingsList
end


% - Subfunctions-


function prettyPrint(bindingsList)
% Print bindingsList nicely
cellfun(@(x)fprintf('%-20s - %-55s -%-20s\n',x{1:2},char(x{3})),bindingsList);


function contexts=listContexts
% List valid MatlabKeyBindingsManager contexts
manager = com.mathworks.services.binding.MatlabKeyBindings.getManager;
contexts = manager.getContexts;
if nargout == 0
    for ii=1:contexts.size,
        disp(char(contexts.get(ii-1).getID));
    end
end

function isValid=isValidContext(context)
% Check that given context exists in MatlabKeyBindingsManager
contexts = listContexts;
isValid = false;
for ii=1:contexts.size,
    if strcmp(char(contexts.get(ii-1).getID),context)
        isValid = true;
        break
    end
end


function customKeyBindingSet = bindShortcuts(newShortcuts,context)
% Prepare and activate a custom key binding set.

% Get Keybindings manager
manager = com.mathworks.services.binding.MatlabKeyBindings.getManager;
currentKBSet = manager.getCurrentKeyBindingSet;
if ~isa(currentKBSet,'com.mathworks.mwswing.binding.CustomKeyBindingSet')
    % Create new custom KeybindingsSet
    try
        customKeyBindingSet = com.mathworks.mwswing.binding.CustomKeyBindingSet(...
            'EditorMacro',currentKBSet,'Customized Editor shortcuts',true,true);
    catch %#ok<*CTCH>
        % Some changes in constructor arguments between R2007b->R2009a
        customKeyBindingSet = com.mathworks.mwswing.binding.CustomKeyBindingSet(...
            'EditorMacro',currentKBSet,'Customized Editor shortcuts');
    end
else
    customKeyBindingSet = currentKBSet;
end
% Context of keybindings set
actContext = manager.getContext(context);
% Add bindigs
for ii = 1:size(newShortcuts,1)
    if ~isempty(newShortcuts{ii,1})
        actionData = manager.getActionData(newShortcuts{ii,1});
    else
        actionData = [];
    end
    % Remove old bindings
    customKeyBindingSet = removeKeybindings(manager,customKeyBindingSet,...
        newShortcuts{ii,2},actionData,actContext);
    % Add new
    if ~isempty(actionData)
        customKeyBindingSet.addCustomKeyBinding(actContext,...
            actionData,newShortcuts{ii,2})
    end
end
customKeyBindingSet.removeGlobalParentDuplicates
try
    manager.addCustomKeyBindingSet(customKeyBindingSet)
catch 
    % Already exist, never mind
end
manager.setCurrentKeyBindingSet(customKeyBindingSet.getID)


function keyBindingSet = removeKeybindings(manager,keyBindingSet,keyStrokes,actionData,context)
% Remove current shortcuts 
try
    kList = com.mathworks.mwswing.binding.KeyStrokeList(keyStrokes);
catch
    % Older versions
    kList = com.mathworks.mwswing.binding.KeyStrokeCollection(keyStrokes);
end
bindingsList = getShortcuts(context.getID);
if isa(actionData,'com.mathworks.mwswing.binding.ActionData'),
    actions = [actionData,[]];
else
    actions = javaArray('com.mathworks.mwswing.binding.ActionData',1);
end
for ii=1:numel(bindingsList)
    if bindingsList{ii}{3}.contains(kList)
        actions(end+1) = manager.getActionData(bindingsList{ii}{1}); %#ok<AGROW>
    end
end
for ii = 1:numel(actions),
    if ~isempty(actions(ii)),
        % Remove actions old binding
        try
            old = keyBindingSet.getKeyBindings(context,actions(ii));
            keyBindingSet.removeCustomKeyBinding(old.get(0),context,actions(ii))
            keyBindingSet.removeGlobalBinding(old.get(0),actions(ii))
        catch
            % No active binding
        end
    end
end


function reloadDocuments()
% Reload all open documents in editor. 
%
% Required for updating shortcuts. Tries to keep current carret
% position.
mDesktop = com.mathworks.mde.desk.MLDesktop.getInstance;
editorViews=mDesktop.getGroupMembers('Editor');
oldActiveDocument = getActiveDocument;
try
    eApp = com.mathworks.mde.editor.MatlabEditorApplication.getInstance;
    for ii = 1:numel(editorViews)
        % getEditor and getDocument are only in > R2009a
        editor = editorViews(ii).getEditor;
        filename = editor.getLongName;
        % Should find current line and column instead and then use
        % openDocumentToLineAndColumn
        caretPos = editor.getCaretPosition;
        editor.closeNoPrompt;
        com.mathworks.mlservices.MLEditorServices.openDocument(filename);
        newEditor = eApp.getActiveEditor;
        newEditor.setCaretPosition(caretPos);
    end
    mDesktop.setClientSelected(oldActiveDocument,true);
catch
    try
        jEditor=getJEditor;
        jMainPane=getJMainPane(jEditor);
        for docIdx=1:jMainPane.getComponentCount
            hEditor = getEditorPane(jMainPane.getComponent(docIdx-1));
            filename = hEditor.getFilename;
            caretPos = hEditor.getCaretPosition;
            lineNum = hEditor.getLineFromPos(caretPos);
            colNum = hEditor.getColFromPos(caretPos);
            com.mathworks.mlservices.MLEditorServices.closeDocument(filename);
            com.mathworks.mlservices.MLEditorServices.openDocumentToLineAndColumn(filename,lineNum,colNum,true);
        end
        mDesktop.setClientSelected(oldActiveDocument,true);
    catch
        % Never mind
    end
end


function readBindingsFromFile(fname)
% Read and activate keybindings from a file
% 
% This works only in R2009a, older versions lack required java methods. 
% Default file is $prefdir/EditorMacroKBSet.xml
% 
if nargin < 1
    fname = [prefdir,filesep,'EditorMacroKBSet.xml'];
end
try
    f=java.io.File(fname);
    manager = com.mathworks.services.binding.MatlabKeyBindings.getManager;
    customSet = com.mathworks.mwswing.binding.CustomKeyBindingSetUtils.readFromFile(f,manager);
    manager.addCustomKeyBindingSet(customSet)
    manager.setCurrentKeyBindingSet(customSet.getID)
catch
    % Never mind
end


function writeBindingsToFile(fname)
% Write current keybindings to a file
% 
% This works only in R2009a, older versions lack required java methods. 
% Default file is $prefdir/EditorMacroKBSet.xml
% 
if nargin < 1
    fname = [prefdir,filesep,'EditorMacroKBSet.xml'];
end
manager = com.mathworks.services.binding.MatlabKeyBindings.getManager;
currentKBSet = manager.getCurrentKeyBindingSet;
try
    f=java.io.File(fname);
    com.mathworks.mwswing.binding.CustomKeyBindingSetUtils.writeToFile(f,currentKBSet)
    fprintf('Wrote current keybindings to file:\n\t%s',fname)
catch
    % Never mind
end


function bindingsList = getShortcuts(context)
% List keyboard shortcuts of given context
%   Context - KeyBindingSets context
manager = com.mathworks.services.binding.MatlabKeyBindings.getManager;
currentKBSet = manager.getCurrentKeyBindingSet;
actContext = manager.getContext(context) ;
actions = manager.getActions.toArray;
IDs = cell(numel(actions),1);
for ii=1:numel(actions)
    IDs{ii} = char(actions(ii).getID);
end
[dummy, sortInd] = sort(IDs);
bindingsList = {};
for ii=sortInd(:)'
    try
        bindingsList{end+1} = {char(actions(ii).getID),...
            char(actions(ii).getDescription),...
            currentKBSet.getKeyBindings(actContext,actions(ii))}; %#ok<AGROW>
    catch
        % Nonsupported actions in Editor Context. Never mind.
    end
end


function activeDocument=getActiveDocument()
% Get document name of active Editor instance
try
    % R2009a
    eApp = com.mathworks.mde.editor.MatlabEditorApplication.getInstance;
    activeDocument = eApp.getActiveEditor.getLongName;
catch
    try
        activeDocument = com.mathworks.mlservices.MLEditorServices.builtinGetActiveDocument;
    catch
        % No can do
        activeDocument = [];
    end
end
    

function modifiedExist=anyModifiedDocs()
% Check for modified documents
modifiedExist = false;
try
    mDesktop = com.mathworks.mde.desk.MLDesktop.getInstance;
    editorViews=mDesktop.getGroupMembers('Editor');
    for ii = 1:numel(editorViews)
        % getEditor and getDocument are only in > R2009a
        if editorViews(ii).getEditor.isDirty
            disp(editorViews(ii).getEditor.getLongName)
            modifiedExist = true;
            break
        end
    end
catch
    docNames = com.mathworks.mlservices.MLEditorServices.builtinGetOpenDocumentNames;
    for ii = 1:numel(docNames)
        if com.mathworks.mlservices.MLEditorServices.isDocumentDirty(docNames(ii))
            modifiedExist = true;
            break
        end
    end
end


% Following functions are from EditorMacro.m
% http://www.mathworks.com/matlabcentral/fileexchange/24615.
%
% Programmed by Yair M. Altman: altmany(at)gmail.com

function myError(id,msg)
% Internal error processing
v = version;
if (v(1) >= '7')
    error(id,msg);
else
    % Old Matlab versions do not have the error(id,msg) syntax...
    error(msg);
end
  
  
function jEditor = getJEditor
% Get the Java editor component handle
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


function hEditorPane = getEditorPane(jDocPane)
% Get EditorPane
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


function jMainPane = getJMainPane(jEditor)
% Get the Java editor's main documents container handle
jMainPane = [];
try
    v = version;
    if (v(1) >= '7')
        for childIdx = 1 : jEditor.getComponentCount
            if isa(jEditor.getComponent(childIdx-1),'com.mathworks.mwswing.desk.DTMaximizedPane') | ...
                    isa(jEditor.getComponent(childIdx-1),'com.mathworks.mwswing.desk.DTFloatingPane')  | ...
                    isa(jEditor.getComponent(childIdx-1),'com.mathworks.mwswing.desk.DTTiledPane')  %#ok Matlab 6 compatibility
                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


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


function keystroke = normalizeKeyStroke(keystroke)
% Normalize the keystroke string to a standard format
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
    
    keystroke = javax.swing.KeyStroke.getKeyStroke(keystroke);  % normalize & check format validity
    keystroke = char(keystroke.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)...
    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

Contact us at files@mathworks.com