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