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

5 views (last 30 days)
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 2 Dec 2017
Edited: Greg on 2 Dec 2017
Edit to remove confusion: This is a good question, very intriguing.
Original text: This is a good one! No promises I'll get to an answer soon, but I'll keep trying.
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
Nick Counts
Nick Counts on 7 Dec 2017
You talked me into it. Quick error fix - your feval(@realcla,varargin{:}); should be feval(realcla,varargin{:}); or Matlab gets fussy :)

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
Greg
Greg on 6 Dec 2017
I can't figure out why
hgfeval(fig.WindowButtonDownFcn{1},fig,evt,fig.WindowButtonDownFcn{2:3});
doesn't accomplish the same thing. From everything I can dig, that's exactly the code that runs when you right-click.
I strongly urge caution when putting a Java Robot into production-level code.
  • The pause timing is typically very finicky (machine and OS task-list dependent)
  • Users can be moving the mouse during the pauses (oh boy)
  • Users can get grumpy about the mouse pointer not being where they left it
  • The default 1:5 plot didn't work for me, the mouse ended up clicking exactly on the line, not the axes (oops, wrong UIContextMenu!) - Are you positive you can guess a pixel on every plot your tool generates that won't have data in that pixel?
Don't get me wrong, I love a good Java Robot. I still have my code that used a Robot to play Microsoft's Solitaire back in XP. Take a screenshot, find pixel location of card to move, click and drag it to valid card, click the deck to draw new cards.
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
Nick Counts
Nick Counts on 4 Dec 2017
Fascinating - I wonder if that is intended behavior? Normally I'm totally into breaking functionality for my own evil purposes, but in this case, I think I'm going with your second option below.
If other people (and they will) start doing some maintenance, I think the broken cla() will be harder to figure out than an intentionally overloaded cla() wrapper that they can find with which
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
Greg
Greg on 6 Dec 2017
So it looks like the plot edit toolbar button sets the figure's WindowButtonDownFcn to @localModeWindowButtonDownFcn while plot edit is active. And, the callback can't be set while plot edit is active. This default callback then uses some switching behavior to apply callbacks to figure children (such as axes uicontextmenu).
I am stumped again.
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.

Categories

Find more on Startup and Shutdown in Help Center and File Exchange

Community Treasure Hunt

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

Start Hunting!