Prompt user before clearing axis when using the "clear axes" context menu
2 views (last 30 days)
Show older comments
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
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.
Accepted Answer
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
More Answers (4)
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
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
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
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.
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
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.
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
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.
See Also
Categories
Find more on Graphics Object Programming 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!