Prompt user before clearing axis when using the "clear axes" context menu
2 次查看(过去 30 天)
显示 更早的评论
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 个评论
Greg
2017-12-4
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.
采纳的回答
Greg
2017-12-4
编辑:Greg
2017-12-8
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 个评论
更多回答(4 个)
Yair Altman
2017-12-6
编辑:Yair Altman
2017-12-13
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
2017-12-5
编辑:Yair Altman
2017-12-6
@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 个评论
Greg
2017-12-6
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
2017-12-6
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
2017-12-2
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 个评论
Greg
2017-12-4
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
2017-12-6
编辑:Greg
2017-12-6
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 个评论
Greg
2017-12-6
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.
另请参阅
类别
在 Help Center 和 File Exchange 中查找有关 Graphics Object Programming 的更多信息
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!