Prompt user before clearing axis when using the "clear axes" context menu

1 view (last 30 days)
Nick Counts
Nick Counts on 2 Dec 2017
Edited: Yair Altman on 13 Dec 2017
Today one of my users inadvertently selected "Clear Axis" (he was aiming for "Paste") while using a tool I wrote and maintain. This action is not reversible using the built in "undo" functionality.
This resulted in a fair amount of frustration and time lost, as the entire figure needed to be repopulated. I thought it would be a simple task to add a questdlg() or some other user interaction ("Are you sure?") or possibly remove the menu item altogether.
I haven't had any luck. It looks like the uicontextmenu is generated when the right-click happens, and I don't seem to be able to override or intercept it.
The callback is @localClearAxis (something like that) which must be an internal private method somewhere in the Matlab toolbox, and I can't seem to overload it, either.
In short, I'm stuck.
I can turn off the 'hittest' property for the axis, but that's a terrible solution because it breaks a lot of useful functionality.
Does anyone have any ideas/suggestions?
Thanks!
  4 Comments
Greg
Greg on 4 Dec 2017
So I came up with a pair of half-workarounds, neither of which I'm satisfied with. I'm close to throwing in the towel, because I found that the right-click-menu for Clear Axes calls plotSelectMode. This is a p-code file, to which there is no included .m file. And it appears to be called through builtin('plotSelectMode',...), because you can't overload it.

Sign in to comment.

Accepted Answer

Greg
Greg on 4 Dec 2017
Edited: Greg on 8 Dec 2017
Another half-answer (pick your poison) I came up with is to overload cla. This clearly has its own dangers, but could be somewhat elegant if you had a unique property value you could query to confirm the axes comes from the tool you wrote and maintain.
function ret_ax = cla(varargin)
% Borrow the input-checking logic from MATLAB's original cla here
% ...
% getappdata, or however you choose to bury your identifier
% or omit if you want to prompt user before clearing axes with ANY method
axappdata = getappdata(ax);
if ~isempty(axappdata) && isfield(axappdata,'MyUniqueID') && strcmp(axappdata.MyUniqueID,'ThisCameFromMyTool!')
rsp = questdlg('Clear the axes?');
if ~strcmp(rsp,'Yes')
return
end
end
%EDIT: per clarification from Steven Lord, builtin cannot be used for cla
% builtin('cla',varargin{:});
% The only other option (and this makes me feel dirty) I can think of
% Store a function handle to the real cla before adding the overloaded cla to the path
realcla = getappdata(groot,'realcla');
feval(realcla,varargin{:});
% Your startup function should then be something like:
function startup
% I think setappdata works with groot...
setappdata(groot,'realcla',@cla);
addpath([path to overloaded cla.m]);
  22 Comments

Sign in to comment.

More Answers (4)

Yair Altman
Yair Altman on 6 Dec 2017
Edited: Yair Altman on 13 Dec 2017
Here is a full code example that works and replaces the "Clear Axes" menu with something else (feel free to modify the label, callback and any other menu property):
% Create an initial figure / axes for demostration purpose
fig = figure('MenuBar','none','Toolbar','figure');
plot(1:5); drawnow;
% Enter plot-edit mode temporarily
plotedit(fig,'on'); drawnow
% Preserve the current mouse pointer location
oldPos = get(0,'PointerLocation');
% Move the mouse pointer to within the axes boundary
% ref: https://undocumentedmatlab.com/blog/undocumented-mouse-pointer-functions
figPos = getpixelposition(fig); % figure position
axPos = getpixelposition(gca,1); % axes position
figure(fig); % ensure that the figure is in focus
newPos = figPos(1:2) + axPos(1:2) + axPos(3:4)/4; % new pointer position
set(0,'PointerLocation',newPos); % alternatives: moveptr(), java.awt.Robot.mouseMove()
% Simulate a right-click using Java robot
% ref: https://undocumentedmatlab.com/blog/gui-automation-robot
robot = java.awt.Robot;
robot.mousePress (java.awt.event.InputEvent.BUTTON3_MASK); pause(0.1)
robot.mouseRelease(java.awt.event.InputEvent.BUTTON3_MASK); pause(0.1)
% Modify the <clear-axes> menu item
hMenuItem = findall(fig,'Label','Clear Axes');
if ~isempty(hMenuItem)
label = '<html><b><i><font color="blue">Undocumented Matlab';
callback = 'web(''https://undocumentedmatlab.com'',''-browser'');';
set(hMenuItem, 'Label',label, 'Callback',callback);
end
% Hide the context menu by simulating a left-click slightly offset
set(0,'PointerLocation',newPos+[-2,2]); % 2 pixels up-and-left
pause(0.1)
robot.mousePress (java.awt.event.InputEvent.BUTTON1_MASK); pause(0.1)
robot.mouseRelease(java.awt.event.InputEvent.BUTTON1_MASK); pause(0.1)
% Exit plot-edit mode
plotedit(fig,'off'); drawnow
% Restore the mouse pointer to its previous location
set(0,'PointerLocation',oldPos);
Note: the code simulates mouse-clicks using a Java Robot instance, as explained here: https://undocumentedmatlab.com/blog/gui-automation-robot
You might want to experiment with different pause values.
Addendum: this solution is further discussed here: https://undocumentedmatlab.com/blog/plotedit-context-menu-customization
  1 Comment
Nick Counts
Nick Counts on 7 Dec 2017
I really appreciate your help. This works with a caveat:
For some reason, I am unable to programmatically move the mouse cursor on my dev machine (maybe others with 2014b?). Maybe it's a Mac issue?
However, when I run your code and move my mouse over the figure, it absolutely works as advertised.
I'm going to call this answered, as it seems to fit the bill. I'm torn over overloading cla() and modifying the menu callback. I prefer the callback in principle, but the details proved far trickier than I expected.

Sign in to comment.


Yair Altman
Yair Altman on 5 Dec 2017
Edited: Yair Altman on 6 Dec 2017
@localClearAxes is an internal function within %matlabroot%/toolbox/matlab/graph2d/private/plotSelectMode.p (as you can immediately see if you run the profiler while doing the action). This is a p-file, so the internal code is not accessible.
The uicontextmenu is installed by the plotedit function (not exactly - it's buried deep inside, but triggered by plotedit) which is called when you click the toolbar button. So instead of fiddling with internal Matlab code, simply wait for plotedit to do its thing and then modify whatever you want. For example:
hEditPlot = findall(gcf,'tag','Standard.EditPlot'); % get plot-edit toolbar button handle
hEditPlot.ClickedCallback = @myPlotEditFunc; % default: 'plotedit(gcbf,'toggle')'
function myPlotEditFunc(varargin)
plotedit(gcbf,'toggle'); % run the standard callback
hMenuItem = findall(gcbf,'Label','Clear Axes');
hMenuItem.Callback = @myRealClearAxesFunc; % default: {@localClearAxes, hUimode}
end
This simple example fixed the toolbar callback, you'd probably want to do the same also for the corresponding main-menu item (if you enabled it in your GUI).
Additional examples of fiddling with the built-in toolbar/menu-bar items:
  4 Comments
Yair Altman
Yair Altman on 6 Dec 2017
calling the uimode-installed callback function fails because it relies on the pointer location and click settings (right/left/center click). Clicking the line doesn't matter because the axes contextmenu is installed even if it is the line that was right-clicked. And if you're worried about moving the pointer, it can be restored to its former position at the end of the process.
Note: I'm moving the code to a separate answer, rather than as a comment.

Sign in to comment.


Greg
Greg on 2 Dec 2017
a = axes(figure);
plot(a,magic(8));
set(a.Children,'HandleVisibility','off');
Or you can selectively set individual children to HandleVisibility off at creation.
Word of caution: apparently this breaks cla (and the right-click clear axes) entirely, until you use cla(a,'reset'); Meaning:
set(a.Children,'HandleVisibility','on');
does not restore regular cla or right-click clear axes functionality. Who knew?
  3 Comments
Greg
Greg on 4 Dec 2017
I would almost guarantee it's not intentional behavior. If I start feeling spry, I'll submit it as a possible bug report.
If you don't mind, vote or accept one of the answers? Beware that'll re-arrange the answers so "... your second option below" won't make sense anymore.

Sign in to comment.


Greg
Greg on 6 Dec 2017
Edited: Greg on 6 Dec 2017
plot([1:4])
hEditPlot = findall(gcf,'tag','Standard.EditPlot')
plotedit(gcbf,'toggle')
hEditPlot.ClickedCallback = @myPlotEditFunc % default: 'plotedit(gcbf,'toggle')'
plotedit(gcbf,'toggle')
Programmatically toggle it on and back off to allow changing the callback.
  7 Comments
Nick Counts
Nick Counts on 7 Dec 2017
Yes, overloading cla sounds better and better.
I think with the potential timing issues and my strange inability to programmatically move my cursor, this is the way to go for my application.
Now I just have to fix my bad path habits!

Sign in to comment.

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!