7

For an user interface I'm programming an uitable. The user chooses an option A,B or C in the first column and the suboption in the second column depends on what was chosen in the first, either A.1,A.2 or A.3 or B.1,B.2 or B.3 or the same for C

enter image description here

The code for the table can be found in Appendix A.

When the user first defines the main option, then automatically the suboptions are reduced accordingly to only valid choices. This is realized by evalulating the CellEditCallback for column 1 and resetting the ColumnFormat for column 2. (function modifySelection in Appendix B) If the user now realizes he made a mistake and needs to edit a suboption another time, then the ColumnFormat is still set according to the previous edited main option and the valid choices are not available unless he re-chooes the main option another time. (see the blue highlighting in picture).

To resolve this, I also implemented the CellSelectionCallback calling the function justifySelection (in Appendix B), which is checking by selection, which option was chosen in column 1 to offer again the right suboptions for column 2. But as this callback reacts on selection, I need to select twice, one time to trigger the CellSelectionCallback and another to actually get my choices. For large tables, this can be very annoying!

So my question is:

Is there a way to prevent the popup menu in column 2 from popping up, until it found out what's the content of the according column 1, so it immediately offers the valid choices?

Or:

How could I detect a mouse click on a cell and get the row and column-index? But without invoking the following selection and popping up action?

I was already raking all available properties but didn't found anything which could be useful. Maybe one could do something using the ButtonDownFcn, but how to get the cell indices? What about the BusyAction property, how can that be used for my purpose?

Any ideas?

I'm sorry in advance to bomb you with so much code, it's already the most minimal example, but fully executable, so you can try it out.


Appendix A/B

function fancyUitable 

selector_1 = { 'A'; 'B' ; 'C' };
selector_2 = { 'first select first row!' };

h = figure('Position',[200 100 268 120],'numbertitle','off','MenuBar','none');

defaultData =  repmat( {'select main option...', 'select suboption...'} ,5,1);
columnname =   {'Option                             ',...
                'Suboption                          '};
columnformat = { {selector_1{:}}, selector_2 };
columneditable =  [true true]; 
t = uitable(h,'Units','normalized','Position',[0 0 1 1],...
              'Data', defaultData,... 
              'ColumnName', columnname,...
              'ColumnEditable', columneditable,...
              'ColumnFormat', columnformat,...  
              'RowName',[],...
              'CellEditCallback',@modifySelection,...
              'CellSelectionCallback',@justifySelection);

set(h,'Tag','Config_figure')
set(t,'Tag','Config_table')
end

%   **Appendix B**
%   (part of the same function file)


function modifySelection(~,evt_edit)
if evt_edit.Indices(2) == 1
    modifyPopup( evt_edit.Indices(1) );
end
end

function justifySelection(~,evt_select)
try  %to surpress an unimportant error
    if evt_select.Indices(2) == 2
        modifyPopup( evt_select.Indices(1) );
    end
end
end

and finally the single function modifyPopup which rewrites the Columnformat:

function  modifyPopup( row )
    id_group_1 = {'A.1';'A.2';'A.3'};
    id_group_2 = {'B.1';'B.2';'B.3'};
    id_group_3 = {'C.1';'C.2';'C.3'};
    id_default = {'select main option first'};

    myfigure = findobj('Tag','Config_figure');
    config_data = get(findobj(myfigure,'Tag','Config_table'),'Data');
    selector = config_data(row,1);
    selector = selector{1};

    config_format = get(findobj(myfigure,'Tag','Config_table'),'ColumnFormat');
    switch selector
        case 'A'
            config_format{2} = id_group_1';
        case 'B'
            config_format{2} = id_group_2';
        case 'C'
            config_format{2} = id_group_3';
        otherwise
            config_format{2} = id_default;
    end
    set(findobj(myfigure,'Tag','Config_table'),'ColumnFormat',config_format)
end

Bounty: Why just +50? - I guess it's either not possible or the answer is easy, once one had the right initial idea. I'm not looking a for a complex workaround using java object properties etc. Thank you in advance!


I include the discussion from the comments here to keep the overview:

If you want to try it out, you can copy the code and follow these steps to reproduce the undesired behaviour:

  1. select main option A in the first row.
  2. the suboption in the first row then contains the choices A.1, A.2 and A.3.
  3. select main option B in the second row, therefore the choices for the suboption in the second row are B.1, B.2 and B.3
  4. BUT NOW you want to change the suboption in the first row (directly); you would expect to get the choices A.1, A.2 and A.3; but you don't. You get offered B.1, B.2 & B.3; - Because the last main option you selected was B (though in a diffrent row).

It seems that instead of looking for the last option, you should look at the relevant option. So either make sure that clicking on a suboption does a 'lookup' to see which main option there is,

Thats exactly what I'm looking for! But how could I do that? How to detect the click, get the column&row indices, set the right ColumnFormat and then finally let the cell pop up. The only possibility I see until now is the CellSelectionCallback, but it is executed after the cell already popped up with the invalid choices. I'd need a kind of ClickedCallback, like there is for pushbuttons

or make sure that selecting a main option only sets the suboptions for that row.

That's not possible, you can't set a suboption for a certain row as you need to modify ColumnFormat, which affects the whole table and not just one row.

Robert Seifert
  • 25,078
  • 11
  • 68
  • 113
  • how about buffering the data: hold the complete data within a variable like FullData and use another one like DisplayedData that is displayed that will be extended when something was selected :) – Lucius II. Oct 16 '13 at 15:29
  • I thought about the same. But I guess it's not possible, because the popups are just realized over the `ColumnFormat` property. And this must necessarily be a row-vector. – Robert Seifert Oct 16 '13 at 15:38
  • perhaps using second uitable could help?... – Lucius II. Oct 21 '13 at 12:15
  • how would that help? I still would need a "ClickedCallback", wouldn't I? – Robert Seifert Oct 21 '13 at 12:49
  • I don't understand the problem. What is the difference between selecting a 'main option' for the first time or for the second time? I guess the selection of the 'main option' should be the thing to trigger the reset. – Dennis Jaheruddin Oct 24 '13 at 11:17
  • @DennisJaheruddin Imagine the following steps: 1) you select main option A in the first row. 2) the suboption in the first row contains the choices A.1, A.2 and A.3. 3) you select main option B in the second row, therefore the choices for the suboptin the second row are B.1, B.2 and B.3 5) BUT NOW you want to change the suboption in the first row (directly) - you would expect to get the choices A.1, A.2 and A.3 - but you don't. You get offered B.1, B.2 & B.3 - Because the last main option you selected was B (though in a diffrent row). Is that clear now? – Robert Seifert Oct 24 '13 at 11:30
  • @thewaywewalk I think I understand the problem now. It seems that instead of looking for the last option, you should look at the relevant option. So either make sure that clicking on a suboption does a 'lookup' to see which main option there is, or make sure that selecting a main option only sets the suboptions for that row. Perhaps the latter can easily be achieved by adding an index to your suboptions set. – Dennis Jaheruddin Oct 24 '13 at 11:33
  • @DennisJaheruddin I included your comments and answered them in the question, to keep the overview – Robert Seifert Oct 24 '13 at 12:07
  • Rather than changing the order in which things happen, would it be possible to force the opening of the dropdown menu again after the right values have been set? Otherwise (assuming the changing operation is not very heavy and you are not worried about backtabbing), you may be able to change the set based on the mouseover status. – Dennis Jaheruddin Oct 24 '13 at 12:54
  • I thought about that also. I'd need to call another "popup" at the end of my CellSelectionCallback, but I don't know how I get access to the function doing this. If I could, I probably could also prevent the fist popup. I wonder which function the table uses internally to invoke the popup. – Robert Seifert Oct 24 '13 at 13:36
  • It sounds to me like you're using the wrong tool for the job...Basically it's the consequence of using a global variable (the `ColumnFormat`), while huge chunks of the machinery are not available to you. You just don't have the fine-grained control over the the specific order of evens that you need for this task. So, *MUST* the second column be part of the table, or could you use, say, regular popups, that all couple to their corresponding row in the table next to it? – Rody Oldenhuis Oct 24 '13 at 15:50
  • That's probably true. It's just the first GUI I'm programming and I was quite lost with GUIDE at the beginning, so I was glad how convenient to use the uitable is. It fits my needs perfectly, except this little missing function in this question... – Robert Seifert Oct 24 '13 at 16:07
  • @thewaywewalk: did you try something like what I suggested? It's probably a lot more hassle to position all the elements right, but I think it's the only way to go here. Oh and, could you include an "@" when replying? Then I get a notification :) – Rody Oldenhuis Oct 25 '13 at 05:33
  • @RodyOldenhuis I will try your suggestion. What makes me a bit trouble with your idea, is that the aim of the whole table is to finally get a matrix from all choices and values in the table (which is much bigger than in the example). If every popup is a single ui-element, or I also would create a uitable for every single row, than I have to merge all ui-elementes to one matrix again. (the size of the table is also dynamic) Which is possible for sure, just don't know if I can make it until the deadline. But definitely an idea worth thinking about! – Robert Seifert Oct 25 '13 at 06:23
  • @thewaywewalk: a computer can make many tedious, time-consuming tasks happen in a fraction of a second; you just have to know *how* :) See my answer. – Rody Oldenhuis Oct 25 '13 at 08:51

3 Answers3

4

Though I highly appreciate the effort and the solution of Rody Oldenhuis and he definetely deserved the award, his solution would have required a lot of changes in my code, so I kept trying to find a simpler solution. Here it is, finally 99% bug-free.

(all code parts within on function script)

function fancyUitable 

close all

%basic properties
line_height = 21.32;
table_height = 6*line_height;
lh = line_height/table_height;
cw = 200; %columnwidth

h = figure('Position',[200 100 2*cw+2 table_height],...
           'numbertitle','off','MenuBar','none');

%header
uitable(h,'Units','normalized','Position',[0 1-lh 1 lh],...
              'ColumnWidth', {cw cw},...              
              'ColumnName', {'Option','Suboption'},...
              'RowName',[]);

%button (currently no icon) to store table
tbar = uitoolbar(h);
uipushtool(tbar,'ClickedCallback',@store);

% addrow(figurehandle,number of row, percentage lineheight)
% every function call creates a new row, later dynamically
addRow(h,1,lh);
addRow(h,2,lh);
addRow(h,3,lh);
addRow(h,4,lh);
addRow(h,5,lh);
end

function edit(src,evt)

if evt.Indices(2) == 1
    modifyPopup( src,evt.Indices(1) );
end

% disables cell selection highlighting, when one jumps to next table,
% a bit laggy though
fh = get(src,'parent');
copyobj(src,fh);
delete(src);

end

function  modifyPopup( src,row )
    id_group_1 = {'A.1';'A.2';'A.3'};
    id_group_2 = {'B.1';'B.2';'B.3'};
    id_group_3 = {'C.1';'C.2';'C.3'};
    id_default = {'select output file first'};

    config_data = get(src,'Data');
    selector = config_data(row,1);
    selector = selector{1};

    config_format = get(src,'ColumnFormat');
    switch selector
        case 'A'
            config_format{2} = id_group_1';
        case 'B'
            config_format{2} = id_group_2';
        case 'C'
            config_format{2} = id_group_3';
        otherwise
            config_format{2} = id_default;
    end
    config_data = { selector , 'select suboption...' };  %reset column 2
    set(src,'Data',config_data);
    set(src,'ColumnFormat',config_format);
end

function addRow(fh,k,lhp)
selector_1 = { 'A'; 'B' ; 'C' };
selector_2 = { 'first select first row!' };

defaultData =  {'select main option...', 'select suboption...'};
columnformat = { {selector_1{:}}, selector_2};
columneditable =  [true true];

th = uitable(fh,'Units','normalized','Position',[0 1-(k+1)*lhp 1 lhp],...
              'Data', defaultData,... 
              'ColumnName', [],...
              'ColumnWidth', {200 200},...
              'ColumnEditable', columneditable,...
              'ColumnFormat', columnformat,...  
              'RowName',[],...
              'Tag','value',...
              'UserData',k,...
              'SelectionHighlight','off',...
              'CellEditCallback',@edit);
end

function store(~,~)
ui = findobj(0,'Type','uitable','Tag','value');
L = numel(ui);
output = cell(L,2);
order = zeros(L,1);
for ii=1:L;
    output(ii,:) = get(ui(ii),'Data');
    order(ii)    = get(ui(ii),'UserData');
end
[~,idx] = sort(order);    %as order of handles unequals displayed order
assignin('base','output',output(idx,:));
end

brings up:

finaltable

Robert Seifert
  • 25,078
  • 11
  • 68
  • 113
4

I would not use a uitable; it's just not suited for this sort of thing.

Here's how I would do it:

function GUIdemo

    %%// Construct GUI

    %// Main figure
    mainFig = figure;
    set(mainFig, 'Color', get(0, 'DefaultUicontrolBackgroundColor'));

    %// Create as many blocks as needed. The only thing you have to do is
    %// figure out the "right" positions for each block
    popupHandles = create_ui_blocks([
        0.00  0.50 1.00  0.35
        0.00  0.15 1.00  0.35]);

    %// This OK button gathers all selected options, and just prints them.
    uicontrol(...
        'style'   , 'pushbutton',...
        'units'   , 'normalized',...
        'parent'  , mainFig,...
        'position', [0.4 0.01 0.2 0.1],...
        'callback', @(~,~)getData(popupHandles),...
        'string'  , 'OK'...
        );


    %%// Helper functions

    %// Create control blocks. Each block is composed of:
    %// - a uipanel as container
    %// - three radio buttons for the main selection
    %// - a corresponding popup or the secondary selection
    function popupHandles = create_ui_blocks(positions)

        %// initialize
        numBlocks = size(positions,1);

        panels = zeros(numBlocks,1);
        groups = zeros(numBlocks,1);
        radios = zeros(numBlocks,3);
        popups = zeros(numBlocks,1);

        %// Build each block
        for ii = 1:numBlocks

            %// The container
            panels(ii) = uipanel(...
                'parent'  , mainFig,...
                'position', positions(ii,:)...
                );

            %// The radio buttons
            groups(ii) = uibuttongroup(...
                'parent'  , panels(ii),...
                'position', [0.05 0.05 0.45 0.9]...
                );
            radios(ii,1) = uicontrol(...
                'style'   , 'radio',...
                'units'   , 'normalized',...
                'string'  , 'A',...
                'parent'  , groups(ii),...
                'position', [0.05 0.66 0.9 0.25]...
                );
            radios(ii,2) = uicontrol(...
                'style'   , 'radio',...
                'units'   , 'normalized',...
                'string'  , 'B',...
                'parent'  , groups(ii),...
                'position', [0.05 0.33 0.9 0.25]...
                );
            radios(ii,3) = uicontrol(...
                'style'   , 'radio',...
                'units'   , 'normalized',...
                'string'  , 'C',...
                'parent'  , groups(ii),...
                'position', [0.05 0.0 0.9 0.25]...
                );

            %// Initially, nothing's selected
            set(groups(ii), 'SelectedObject',[]);

            %// The popups
            popups(ii) = uicontrol(...
                'style'   , 'popup',...
                'units'   , 'normalized',...
                'parent'  , panels(ii),...
                'position', [0.55 0.4 0.4 0.2],...
                'string'  , 'Select main option',...
                'enable'  , 'off'...
                );

            %// On changing radiobutton, correct the string list of the popups
            set(groups(ii),'SelectionChangeFcn', @(~,~)selectionChangeCallback(ii));

            %// This is needed by the OK button callback
            popupHandles = popups;

        end

        %// What happens when clicking a radio button?
        %// NOTE: this is a doubly-nested function
        function selectionChangeCallback(num)
            switch get(groups(num), 'SelectedObject')
                case radios(num,1)
                    set(popups(num), 'string', {'A.1', 'A.2', 'A.3'}, 'enable', 'on');
                case radios(num,2)
                    set(popups(num), 'string', {'B.1', 'B.2', 'B.3'}, 'enable', 'on');
                case radios(num,3)
                    set(popups(num), 'string', {'C.1', 'C.2', 'C.3'}, 'enable', 'on');
                otherwise
                    %// ...
            end
        end

    end

    %// What happens when pressing the OK button?
    function data = getData(popupHandles)
        data = char(cellfun(@(x,y)x{y}, ...
            get(popupHandles, 'String'),...
            get(popupHandles, 'Value'),...
            'UniformOutput', false))         %#ok<NOPRT> //
    end

end

enter image description here enter image description here

Output in the MATLAB command window when pressing "OK":

data =
    A.1
    B.1

The layout is of course still crude, but you get the idea. Of course, the radio buttons can also be replaced by a popup (more compact), three pushbuttons, or whatever else you like.

The contents of the popups are not related to each other, which is exactly the problem with the uitable approach. In this GUI, changes in the popup's contents can be instantaneous when changing a radio button, simply because you have better control over how to deal with changes.

A programming note: I personally don't like it when handles of individual components in what I treat as a "block" are floating around in the top-level function, which is why I use doubly-nested functions -- it's kind of like encapsulation. Now, when used outside of classes, this is not everyone's cup of tea, so you might want to convert them. Of course, all nested functions are trivially converted to subfunctions; you just have to manually pass a lot more information around.

With this approach, you lose some functionality (the ability to re-size your UI elements), but you gain intuitive behavior of the GUI controls. When these are the choices, I've been trained to develop towards the latter option. The nice bells and whistles will only impress the end-user the first few times round, but the program's basic functionality will become more and more important with increased usage. As you noted yourself, this buggy behavior gets annoying when you have to use the tool a lot; I'd say, drop the resizability in favor of improved control behavior.

Rody Oldenhuis
  • 37,726
  • 7
  • 50
  • 96
  • I highly appreciate your effort and will follow your suggestion in the future. (+1) I start to agree that my desired functionalty can only be obtained with multiple ui-objects. But it will probably blow up my code enormously. You just have to imagine that I have about 10 main options and 20 dependent suboptions (so there must be a popup for the main option also for sure), furthermore two more coloums with popups (independent though). And I finally need the informations of all columns merged together. So I guess my code will be 5-10 times longer, than my buggy, but working interim solution. – Robert Seifert Oct 25 '13 at 09:05
  • ... but thank you very much for the time you spent, I'll post my final solution regarding your ideas later! – Robert Seifert Oct 25 '13 at 09:07
  • @thewaywewalk: Yep, that's the thing about programming UIs; it's usually very "low density code" -- you need a lot of it to make things happen. But, if I understand you correctly, you need a bunch of main popups, each with 10 entries, and related to that two popups with suboptions, each 20 entries or so. You can do that with my code already; my code iteratively generates whole "rows" that are each fully functional. You just need to design one of these "rows" and you're done. Concatenation of the results should also not be too difficult... – Rody Oldenhuis Oct 25 '13 at 09:14
  • @thewaywewalk: I'm more than willing to help. Perhaps we could exchange e-mail addresses and continue this via e-mail? Mine is (at)(dot). – Rody Oldenhuis Oct 25 '13 at 09:15
  • "you lose some functionality (the ability to re-size your UI elements), " I probably misstated that, the only thing which is dynamic is the number of rows. But this feature is essential. – Robert Seifert Oct 25 '13 at 09:16
  • @thewaywewalk: What is the number of rows based on? – Rody Oldenhuis Oct 25 '13 at 09:17
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/39986/discussion-between-rody-oldenhuis-and-thewaywewalk) – Rody Oldenhuis Oct 25 '13 at 09:18
-2

The solution is to use the Cell Selection Callback with two UITables in your GUI. Make the first table hold the data of {'a','b','c'} then in the cell selection callback, make the second UITable visible and set its data based on the cell selection properties of the first UITable. This link should have everything you need if you look down at the line 'A little hack not needing findjobj'

http://www.mathworks.com/matlabcentral/newsreader/view_thread/306392

  • Sorry, I don't see anything in your link which is not already implemented in my code. I select something in column 1 and column 2 is updated. Thats not the point, that's already working perfectly. The problem appears when I want to modify a cell in column 2 after modification of another cell in column 2. What do you mean with "two tables" - are they next to each other, so to say one table for each column or are they supposed to overlap? – Robert Seifert Oct 21 '13 at 21:07
  • I guess your mistaken belief is, that I want to update `'Data'` in column 2. But I need to update `'ColumnFormat'`. Which is a row vector and not a matrix. – Robert Seifert Oct 21 '13 at 21:16