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

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 Kommentare

How did you determine that the callback is @localClearAxis (or similar)? I'm usually able to dig into that kind of stuff, but I'm having no luck on this one.
Yeah, it's a pain to find. You have to spawn a context menu or the object isn't findable. Furthermore, it looks like they are re-generated each time you right click.
Try this code - if you follow the directions, it should get you to what I found :)
fig = figure;
hp1 = plot([1:10]);
obj = findall(fig, 'Type', 'Axes');
obj.UIContextMenu % You will see an empty result
% You need to right-click in the plot with the select tool or you won't
% see the context menu
input('Right click in the plot with the select tool and then press enter here')
findall(fig)
ca = findall(fig, 'tag', 'ScribeGenericActionClearAxes'
Greg
Greg am 2 Dez. 2017
Bearbeitet: Greg am 2 Dez. 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.
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.

Melden Sie sich an, um zu kommentieren.

 Akzeptierte Antwort

Greg
Greg am 4 Dez. 2017
Bearbeitet: Greg am 8 Dez. 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 Kommentare

Another (less robust) option to burying an identifier is to use dbstack. When called from the right-click-menu Clear Axes option, the call stack has plotSelectMode immediately above cla. I don't recommend this option over using an identifier in your tools, but it does have the advantage that it's a one-stop fix (no need to update your tools to implement an identifier).
I love this.
Quick follow up: does it work on your machine? I get errors from builtin
Error using builtin
Cannot find builtin function 'cal'
Weird, right? which returns cla in the graphics toolbox, just like I'd expect.
Greg
Greg am 4 Dez. 2017
Bearbeitet: Greg am 4 Dez. 2017
You misspelled cla.
But to point, I have to be honest. I don't think I tried it on my machine, just typed up the answer and hit submit.
That was autocorrect being a jerk (but I wouldn't put it past me to have misspelled it!)
Check this out:
That's correct. The cla function is not a built-in function. I know some people use the term built-in to include any function included as part of MATLAB, but that is not how the term is defined for purposes of which functions you can call using builtin. For that purpose, the term refers to functions implemented as part of the built-in source code for MATLAB, not functions implemented as MATLAB function files.
For comparison, look at which sin or which plot. Both sin and plot are built-in functions that can be called using builtin since they are implemented not as MATLAB function files but in the built-in source code for MATLAB.
Nick Counts
Nick Counts am 4 Dez. 2017
Bearbeitet: Nick Counts am 4 Dez. 2017
Thanks, Steve! I learned something new.
I don't suppose you have any ideas for me regarding this question? :)
I tried to copy and modify the contents of TMW's cla (Is that kosher? Even that's troublesome because claNotify and clo aren't accessible from user space. They must be private methods or in the MATLAB source?
Ahh, sorry then. I just saw the error message said 'cal' and figured that's how your error message came back.
Thanks Steven, I never knew that subtlety about builtin.
Check my answer. Everything below "%EDIT:" is new to address the builtin problem.
It's been quite a while since I've done much of any app creation work, so I don't have any suggestions on how to (or if you can) override that context menu.
My best suggestion for you is to contact Technical Support using the Contact Us link in the upper-right corner of the page. The Support staff may know how to do what you want, can ask the Development staff if they don't know, and can enter an enhancement request (using your use case as motivation) if it's not possible right now (or if the way to do it right now is more complicated than it should be.)
Have you tried your updated solution? I might be implementing it incorrectly, but I get into an infinite overloaded cla loop!
I'll mess with it some more
I tried it at the command line to confirm (didn't trust my memory) that the function handle still called the original cla. I'll try the full solution in a couple minutes.
Works just fine for me. Unrealized hiccup is that plot(...) calls cla so you get the popup request...
I get the infinite loop when my overloaded cla is on the MATLAB path before I create the function handle.
That's funny. I appreciate your digging into this so much. the standard "installation" for our tool is to add all folders/subfolders to the path, so I'll have to play some path games to make this work.
It's a little wonky, but should be usable. Thanks again
I'm going to leave it open for another few days in case a miracle happens and someone (Yair :p ) tells me about axes.promptUserBeforeClearing=true or something like that.
I learned long ago not to save pathdef. I force my team to have addpath(genpath([tools directory])) in their startup.m files.
I'm thrilled to help. This is one of very few questions that was actually intellectually stimulating. I also expect it won't be long before a user of mine asks for the exact same solution, so I'll be ahead of the curve. Thank you!
I am surprised that Yair hasn't made an appearance on the subject.
Yair hasn't made an appearance because
  1. He's too busy to read all the zillion of new Answers threads
  2. Until now nobody mentioned his name (which triggers a soft alert)
  3. And nobody bothered to post a comment on his UndocumentedMatlab.com blog
And here I thought you had a sixth sense for these types of questions without needing to read all zillion posts. :)
This is the approach I decided to take. I ahem borrowed an idea from Rody Oldenhuis's function_handle submission on the file exchange also.
I now have a cla.m function that checks to see if the callback object has a label "Clear Axes" and prompts the user if it does. Otherwise, it falls through and creates a function handle to Matlab's function (changes directory, sets handle, executes, returns).
This may be a little sloppier than fixing my path issues - but hey, one nightmare at a time, right?
function ret_ax = cla(varargin)
% cla() is an overload for TMW's cla() that prompts a user before clearing irrevocably
if nargin>0 && length(varargin{1})==1 && ishghandle(varargin{1}) && strcmpi((get(varargin{1},'Type')),'axes')
% If first argument is a single axes handle, apply CLA to these axes
ax = varargin{1};
extra = varargin(2:end);
else
% Default target is current axes
ax = gca;
extra = varargin;
end
%%Ask user "Are you sure?" if the call originated from the "Clear Axis" menu item
cbo = gcbo;
if ~isempty(cbo) && isprop(cbo, 'Label') && strcmpi(cbo.Label, 'Clear Axes')
debugout('User selected Clear Axes from context menu');
yesButton = 'Clear Axes';
noButton = 'Cancel';
defaultButton = noButton;
ButtonName = questdlg('This action can not be undone. Do you want to clear the axes?', ...
'Are you sure?', ...
yesButton, noButton, defaultButton);
switch ButtonName
case yesButton
otherwise
return
end % switch
else
debugout('Not a protected axes')
end
%%Return to TMW's cla() function
prevDir = pwd;
cleanup = onCleanup(@(~)cd(prevDir));
realCla = fullfile(toolboxdir('matlab'), 'graphics', 'cla.m');
[pth,name] = fileparts(realCla);
cd(pth);
hcla = str2func(['@' name]);
hcla(ax)
if (nargout ~= 0)
ret_ax = ax;
end
I left the debugout() calls in because I like my debug message tool and hope it is useful to someone else!
Thank you both for all the time you spent on this. I really appreciate such high level help.
Cheers
(edit: added isprop(cbo, 'Label') check to prevent errors when cla is invoked by an object without the 'Label' property)
If you're CD'ing in, creating a handle and CD'ing out, why not just call cla(varargin) while you're in the default directory? I won't lecture on using CD in production code, but I'd be scared.
Again, it was a thrilling question and I completely enjoyed the ride, thank you.
If nothing else, putting the CD/handle/CD in your startup script would be a lot safer than in the actual overloaded cla file. But it requires that startup script on every user's machine.
I started to answer and realized I don't have a good reason. Your point is well taken, and we really should be using startup scripts.
You talked me into it. Quick error fix - your feval(@realcla,varargin{:}); should be feval(realcla,varargin{:}); or Matlab gets fussy :)
Good catch. It could probably also be realcla(varargin{:});

Melden Sie sich an, um zu kommentieren.

Weitere Antworten (4)

Yair Altman
Yair Altman am 6 Dez. 2017
Bearbeitet: Yair Altman am 13 Dez. 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 Kommentar

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.

Melden Sie sich an, um zu kommentieren.

Yair Altman
Yair Altman am 5 Dez. 2017
Bearbeitet: Yair Altman am 6 Dez. 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 Kommentare

Yair, thank you very much for your insights - I like this approach a lot. I'm still having an issue, though (I'm clearly not at your level, so it is likely PEBKAC)
The UIContextMenu and menu item ( 'tag' 'ScribeGenericActionClearAxes' ) simply do not exist for me to set their callback properties.
I'm running r2014b, so perhaps it's a version compatibility issue... I've tried debug stops, setting properties, all kinds of tricks thinking it was a timing issue, but no luck.
Only once I spawn the actual UIContextMenu (anytime after initial spawn) can the MenuItems be found.
Did your code work for you? (I'm sure you tossed that example off quickly and hEditPlot vs hPlotEdit was a typo)
Here is what I have tried:
% In a test script:
plot([1:4])
hEditPlot = findall(gcf,'tag','Standard.EditPlot')
hEditPlot.ClickedCallback = @myPlotEditFunc % default: 'plotedit(gcbf,'toggle')'
% My callback functions
function myPlotEditFunc(varargin)
plotedit(gcbf,'toggle'); % run the standard callback
hUICM = findall(gcbf, 'Type', 'UIContextMenu') % empty :(
ca = findall(gcbf, 'tag', 'ScribeGenericActionClearAxes') % empty :(
end
function promptForClearAxes (varargin)
userAnswer = questdlg('Are you sure?', 'Clear Axis', 'Yes', 'No', 'No')
% do stuff
end
If I spawn an axis and then run ca = findall(gcf, 'tag', 'ScribeGenericActionClearAxes') and ca.Callback = @myclearaxes at the command window, everything works as expected.
I think I'm in a Catch-22
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
% Simulate a right-click using Java robot
% ref: https://undocumentedmatlab.com/blog/gui-automation-robot
fp = getpixelposition(fig); % figure position
ap = getpixelposition(gca,1); % axes position
figure(fig); % ensure that the figure is in focus
np = fp(1:2) + ap(1:2) + ap(3:4)/4; % new pointer position
set(0,'PointerLocation',np);
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',np+[-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
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.
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.
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.

Melden Sie sich an, um zu kommentieren.

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 Kommentare

Greg
Greg am 2 Dez. 2017
Bearbeitet: Greg am 2 Dez. 2017
I'm still going to dig for an answer to hijacking the uicontextmenu, but this should accomplish half of the original need.
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
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.

Melden Sie sich an, um zu kommentieren.

Greg
Greg am 6 Dez. 2017
Bearbeitet: Greg am 6 Dez. 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 Kommentare

Crud, this shouldn't be an answer it should be a comment but I'm on my phone right now so I'm not going to take care of it until tomorrow.
Nick Counts
Nick Counts am 6 Dez. 2017
Bearbeitet: Nick Counts am 6 Dez. 2017
I still can't make headway with these approaches - out of curiosity, what specifically do you mean by the "it" I am toggling?
If it's the plot edit mode, then I'm not sure that actually gets me any closer. It looks like the objects I need to find are created/restored in graph2d/private/plotSelectMode>localRestoreUIContextMenu
When Yair said "uicontextmenu is installed by the plottedit" I think he pointed me towards a fundamental issue: the uicontext menu doesn't seem to actually get instantiated until a right-click ( graph2d/private/plotSelectMode>localHittest or graph2d/private/plotSelectMode>localWindowButtonDownFcn)
So... I don't think I can access the UIContextMenu I need until after a user interaction. I could test for the UIContextMenu's existence in myPlotEditFunc and set my callback once it's there. Then I just hope that the first right-click doesn't lead to an inadvertent "Clear Axes"!
The other (distinct) possibility is that I'm being completely obtuse and you have both solved my problem 3 times over :)
Sorry if that's the case!
"It" is plot edit mode. Running plotedit(gcbf,'toggle') is the same as the user clicking the "Edit Plot" toolbar button.
I swear it worked an hour ago, but no dice now.
I wonder if you set the property after the menu had already been instantiated. I've done that a few times and had a false sense of victory!
Overloading cla sounding better?
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.
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!

Melden Sie sich an, um zu kommentieren.

Kategorien

Mehr zu Environment and Settings finden Sie in Hilfe-Center und File Exchange

Gefragt:

am 2 Dez. 2017

Bearbeitet:

am 13 Dez. 2017

Community Treasure Hunt

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

Start Hunting!

Translated by