legendflex-pkg icon indicating copy to clipboard operation
legendflex-pkg copied to clipboard

resize callbacks broken after save/load .fig

Open buck2202 opened this issue 7 years ago • 4 comments

I'm using legendflex in thousands of programmatically generated figures; I typically save both a matlab .fig and a .png for each, but sometimes the png ends up being inappropriately sized for its end use. In that case, I'll load the fig and reprint it as needed. I discovered by accident that when loading/resizing a saved .fig file, my legendflex-es were not moving relative to their resized anchor objects. On a figure resize event, updatelegfigresize did get called but not updatelegpos, so the legend wasn't moving.

There might be a better way to do this, but my quick fix here was to add a subfunction duplicating your listener creation

function resetListeners(src,evt, hg2flag, Lf,hnew)
addlistener(hnew.leg, 'Position', 'PostSet', @(src,evt) updatelegappdata(src,evt,hnew.leg));
if hg2flag && strcmp(Lf.ref.Type, 'figure')
    addlistener(Lf.ref, 'SizeChanged', @(src,evt) updatelegpos(src,evt,hnew.leg));
else
    addlistener(Lf.ref, 'Position', 'PostSet', @(src,evt) updatelegpos(src,evt,hnew.leg));
end

and then tie that function into the figure CreateFcn callback (duplicating your logic from the ResizeFcn case)

createfcn = get(figh, 'CreateFcn');
ff = @(src,evt)resetListeners(src,evt, hg2flag, Lf,hnew);
if isempty(createfcn)
    set(figh, 'CreateFcn', ff);
else
    if ~iscell(createfcn)
        createfcn={createfcn};
    end
    hasprev = cellfun(@(x) isequal(x, ff), createfcn);
    if ~hasprev
        createfcn = {createfcn{:} ff};
        set(figh, 'CreateFcn', {@wrapper, createfcn})
    end
end

I haven't tested this extensively, but it seems to work as intended on 2016a.

Thanks for this extremely useful piece of code!

buck2202 avatar Dec 10 '17 05:12 buck2202

The CreateFcn hook above did not quite work. In a case where I was repeatedly resetting/reusing the same figure handle (calling clf) and had a separate CreateFcn already present, I ended up getting a deep, repeated nesting of the handle array. clf apparently does not clear the callback functions. After the logic executes once, @wrapper is present as the first element of the xFcn cell array and the target function handle is buried in element 2, which is itself a cell array. Additionally, my anonymous function handle ff needs to be recreated each time to update the buried references to hg2flag, Lf, and hnew.

I think the below will handle all of the valid cases for an already-present CreateFcn (function handle, cell array w/first element as function handle, or string valid for eval). There's probably a cleaner solution, but this is working for me.

createfcn = get(figh, 'CreateFcn');
ff = @(src,evt)resetListeners(src,evt, hg2flag, Lf,hnew);
if isempty(createfcn)
    set(figh, 'CreateFcn', ff);
else
    if numel(createfcn)==1
        if ischar(createfcn)
            createfcn = {@(src,evt)eval(createfcn)};
        elseif isa(createfcn,'function_handle')
            hasprev = strcmp(func2str(createfcn),func2str(ff));
            createfcn = {createfcn};
        else
            % invalid existing data
        end
        
        if ~hasprev
            createfcn = [createfcn(:)' {ff}];
            set(figh, 'CreateFcn', {@wrapper, createfcn})
        end
    elseif iscell(createfcn) && isequal(createfcn{1},@wrapper) && (numel(createfcn)==2)
        ff_ = func2str(ff);
        hasprev = cellfun(@(x) strcmp(func2str(x),ff_), createfcn{2});
        if any(hasprev)
            createfcn{2}{hasprev} = ff;
        else
            %not sure how one would get here, but...
            createfcn{2}{end+1} = ff;
        end
        set(figh, 'CreateFcn', createfcn)
    else
        % invalid existing data
    end
end

Your already-present hasprev check for the ResizeFcn might present my first issue in certain scenarios...I think the @wrapper entry will always throw off that check. I'd suggest something like

hasprev = isequal(rsz,{@updatelegfigresize}) || ...
        ((numel(rsz)==2) && isequal(rsz{1},@wrapper) && any(cellfun(@(x) isequal(x,@updatelegfigresize),rsz{2})));

buck2202 avatar Dec 11 '17 05:12 buck2202

One last edit...the above missed recreating the function handle for single-CreateFcn cases in reused figures.

createfcn = get(figh, 'CreateFcn');
ff = @(src,evt)resetListeners(src,evt, hg2flag, Lf,hnew);
if isempty(createfcn)
    set(figh, 'CreateFcn', ff);
else
    if numel(createfcn)==1
        if ischar(createfcn)
            createfcn = {@(src,evt)eval(createfcn)};
        elseif isa(createfcn,'function_handle')
            hasprev = strcmp(func2str(createfcn),func2str(ff));
            createfcn = {createfcn};
        else
            % invalid existing data
        end
        
        if ~hasprev
            createfcn = [createfcn(:)' {ff}];
            set(figh, 'CreateFcn', {@wrapper, createfcn})
        else
            set(figh, 'CreateFcn', ff)
        end
    elseif iscell(createfcn) && isequal(createfcn{1},@wrapper) && (numel(createfcn)==2)
        ff_ = func2str(ff);
        hasprev = cellfun(@(x) strcmp(func2str(x),ff_), createfcn{2});
        if any(hasprev)
            createfcn{2}{hasprev} = ff;
        else
            %not sure how one would get here, but...
            createfcn{2}{end+1} = ff;
        end
        set(figh, 'CreateFcn', createfcn)
    else
        % invalid existing data
    end
end

buck2202 avatar Dec 19 '17 21:12 buck2202

Thanks for bringing this to my attention. I'll play around with your code suggestion in my test cases and merge it into the main branch if all works well.

kakearney avatar Dec 19 '17 21:12 kakearney

Hi again...two more scenarios. If there is more than one legendflex in a figure, my kludge only worked for the most recently added, and, regardless of the number of legends, the callbacks/listeners are non-functional after copyobj on the parent figure because appdata is not copied.

For the first issue, the problem stemmed from using func2str to determine if the resetListeners callback was already present--under that scenario, I was using a single figure handle to plot, clf, repeat...so after clf, the prior callback would be broken but "look" like the one I wanted only one of. I've removed this shallow duplicate check altogether, and will have to just remember to manually clear out these figure callbacks after clf.

createfcn = get(figh, 'CreateFcn');
ff = @(src,evt)resetListeners(src,evt, hg2flag, Lf,hnew);
if isempty(createfcn)
    set(figh, 'CreateFcn', ff);
else
    if numel(createfcn)==1
        if ischar(createfcn)
            createfcn = {@(src,evt)eval(createfcn)};
        elseif isa(createfcn,'function_handle')
            createfcn = {createfcn};
        else
            % invalid existing data
        end
        
        createfcn = [createfcn(:)' {ff}];
        set(figh, 'CreateFcn', {@wrapper, createfcn})
    elseif iscell(createfcn) && isequal(createfcn{1},@wrapper) && (numel(createfcn)==2)
        createfcn{2}{end+1} = ff;
        set(figh, 'CreateFcn', createfcn)
    else
        % invalid existing data
    end
end

Without doing more digging, I don't have a good suggestion for the copyobj issue. It feels like all of these issues would go away if the legendflex data lived somewhere within the actual graphics object tree, but I'm not familiar enough with the graphics system to understand when/if matlab will translate handle references within the copy process.

buck2202 avatar Aug 03 '18 23:08 buck2202