Skip to content

Commit 335642a

Browse files
committed
Better coverage of Changed event
1 parent 57ba45c commit 335642a

File tree

5 files changed

+138
-17
lines changed

5 files changed

+138
-17
lines changed

+eui/ConditionPanel.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ function deleteSelectedConditions(obj)
169169
obj.makeGlobal;
170170
else % Otherwise delete the selected conditions as usual
171171
obj.ParamEditor.Parameters.removeConditions(rows);
172+
notify(obj.ParamEditor, 'Changed')
172173
end
173174
% Refresh the table of conditions
174175
obj.fillConditionTable();

+eui/FieldPanel.m

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
props.HorizontalAlignment = 'left';
4646
props.UIContextMenu = obj.ContextMenu;
4747
props.Parent = obj.UIPanel;
48-
label = uicontrol('Style', 'text', 'String', name, props);
48+
props.Tag = name;
49+
title = obj.ParamEditor.Parameters.title(name);
50+
label = uicontrol('Style', 'text', 'String', title, props);
4951
if nargin < 3
5052
ctrl = uicontrol('Style', 'edit', props);
5153
end
@@ -56,6 +58,14 @@
5658
end
5759

5860
function onEdit(obj, src, id)
61+
% ONEDIT Callback for edits to field controls
62+
% Updates the underlying parameter struct, changes the UI
63+
% value/string and changes the label colour to red. The src object
64+
% should be the edit or checkbox ui control that has been edited,
65+
% and id is the unformatted parameter name (stored in the Tag
66+
% property of the label and control elements).
67+
%
68+
% See also ADDFIELD, EUI.PARAMEDITOR/UPDATE
5969
disp(id);
6070
switch get(src, 'style')
6171
case 'checkbox'
@@ -67,11 +77,14 @@ function onEdit(obj, src, id)
6777
newValue = obj.ParamEditor.update(id, get(src, 'string'));
6878
set(src, 'String', obj.ParamEditor.paramValue2Control(newValue));
6979
end
70-
changed = strcmp(id,{obj.Labels.String});
80+
changed = strcmp(src.Tag,{obj.Labels.Tag});
7181
obj.Labels(changed).ForegroundColor = [1 0 0];
7282
end
7383

7484
function clear(obj, idx)
85+
% CLEAR Delete a parameter field
86+
% Deletes the label and control elements at index idx. If no index
87+
% given, all controls are deleted.
7588
if nargin == 1
7689
idx = true(size(obj.Labels));
7790
end
@@ -99,18 +112,24 @@ function makeConditional(obj, name)
99112
selected = obj.ParamEditor.getSelected();
100113
if isa(selected, 'matlab.ui.control.UIControl') && ...
101114
strcmp(selected.Style, 'text')
102-
name = selected.String;
115+
name = selected.Tag;
103116
else % Assume control
104-
name = obj.Labels([obj.Controls]==selected).String;
117+
name = obj.Labels([obj.Controls]==selected).Tag;
105118
end
106119
end
107-
idx = strcmp(name,{obj.Labels.String});
120+
idx = strcmp(name,{obj.Labels.Tag});
108121
assert(~ismember(name, {'randomiseConditions'}), ...
109122
'%s can not be made a conditional parameter', name)
110123

111124
obj.clear(idx);
125+
% FIXME The below code could be in a makeConditional method of
126+
% eui.ParamEditor, thus more clearly separating class functionality:
127+
% Editing the exp.Parameters object directly should only be done by
128+
% ParamEditor. This would also make subclassing these panel classes
129+
% more straightforward
112130
obj.ParamEditor.Parameters.makeTrialSpecific(name);
113131
obj.ParamEditor.ConditionalUI.fillConditionTable();
132+
notify(obj.ParamEditor, 'Changed');
114133
obj.onResize;
115134
end
116135

+eui/MControl.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ function loadParamProfile(obj, profile)
321321
end
322322

323323
function paramChanged(obj)
324+
% PARAMCHANGED Indicate to user that parameters have been updated
325+
% Changes the label above the ParamEditor indicating that the
326+
% parameters have been edited
324327
s = get(obj.ParamProfileLabel, 'String');
325328
if ~strEndsWith(s, '[EDITED]')
326329
set(obj.ParamProfileLabel, 'String', [s ' ' '[EDITED]'], 'ForegroundColor', [1 0 0]);

+eui/ParamEditor.m

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
11
classdef ParamEditor < handle
2-
%UNTITLED2 Summary of this class goes here
3-
% ParamEditor deals with setting the paramters via a GUI
2+
%PARAMEDITOR GUI for visualizing and editing experiment parameters
3+
% ParamEditor deals with setting the paramters via a GUI. In general
4+
% this class is involved in constructing a UI comprising a Global UI
5+
% panel, handled by the EUI.FIELDPANEL class, and a Condition table,
6+
% handled by the EUI.CONDITIONPANEL class. It is also responsible for
7+
% updating the underlying EXP.PARAMETERS object and notifying
8+
% downstream listeners of parameter changes.
9+
%
10+
% See also EUI.FIELDPANEL, EUI.CONDITIONPANEL
411

512
properties
13+
% An exp.Parameters object, which keeps track of all parameter changes
614
Parameters
715
end
816

917
properties (Access = {?eui.ConditionPanel, ?eui.FieldPanel})
10-
UIPanel
18+
% Handle to the EUI.FIELDPANEL object, which manages the display of the
19+
% Global parameters
1120
GlobalUI
21+
% Handle to the EUI.CONDITIONPANEL object, which manages the display of
22+
% the trial conditions within a ui table
1223
ConditionalUI
24+
% Handle to the parent container for the ParamEditor. If constructor
25+
% called with no parant input, then this will be a figure handle, the
26+
% same as Root
1327
Parent
28+
% Handle to the figure within which the ParamEditor is displayed
1429
Root
30+
% A listener for changes to the figure size. See also ONRESIZE
1531
Listener
1632
end
1733

1834
properties (Dependent)
35+
% Flag for making editor read only by disabling all UI controls
1936
Enable
2037
end
2138

2239
events
40+
% Event notified each time a user makes an edit to a parameter
2341
Changed
2442
end
2543

2644
methods
2745
function obj = ParamEditor(pars, parent)
46+
% PARAMEDITOR GUI for visualizing and editing experiment parameters
47+
% The input pars is expected to be an instance of the exp.Parameters
48+
% class. Parant is a handle to a parent figure or UI Panel. If no
49+
% parant is given, the editor is created in a new figure.
50+
%
51+
% See also EUI.FIELDPANEL, EUI.CONDITIONPANEL
2852
if nargin == 0; pars = []; end
2953
if nargin < 2
3054
parent = figure('Name', 'Parameters', 'NumberTitle', 'off',...
@@ -55,6 +79,10 @@
5579
end
5680

5781
function delete(obj)
82+
% DELETE Deletes all panels
83+
% Called when the ParamEditor object is deleted or its parant figure
84+
% is closed. Deletes all UI elements and data.
85+
% See also CLEAR
5886
delete(obj.GlobalUI);
5987
delete(obj.ConditionalUI);
6088
end
@@ -79,11 +107,27 @@ function delete(obj)
79107
end
80108

81109
function clear(obj)
110+
% CLEAR Clear the Global and Condition panels
111+
% Deletes all UI fields (labels and control elements) and clears the
112+
% Condition Table data.
113+
%
114+
% See also BUILDUI
82115
clear(obj.GlobalUI);
83116
clear(obj.ConditionalUI);
84117
end
85118

86119
function buildUI(obj, pars)
120+
% BUILDUI Populate Global and Condition UI panels with paramter set
121+
% Clears any existing fields and the condition table, then loads new
122+
% UI controls and labels for the Global parameters, and fills the
123+
% condition table. The input pars must be an instance of the
124+
% exp.Parameters class.
125+
%
126+
% RandomiseConditions is not added as a field but instead is
127+
% represented as a context menu item
128+
%
129+
% See also EXP.PARAMETERS, EUI.FIELDPANEL/ADDFIELD,
130+
% EUI.CONDITIONPANEL/FILLCONDITIONTABLE
87131
obj.Parameters = pars;
88132
obj.clear() % Clear the current parameter UI elements
89133
if isempty(pars); return; end % Nothing to build
@@ -95,7 +139,7 @@ function buildUI(obj, pars)
95139
if strcmp(nm, 'randomiseConditions'); continue; end
96140
if islogical(pars.Struct.(nm{:})) % If parameter is logical, make checkbox
97141
ctrl = uicontrol('Parent', c.UIPanel, 'Style', 'checkbox', ...
98-
'Value', pars.Struct.(nm{:}));
142+
'Value', pars.Struct.(nm{:}), 'Tag', nm{:});
99143
addField(c, nm{:}, ctrl);
100144
else % Otherwise create the default field; a text box
101145
[~, ctrl] = addField(c, nm{:});
@@ -166,6 +210,12 @@ function addEmptyConditionToParam(obj, name)
166210
end
167211

168212
function newValue = update(obj, name, value, row)
213+
% UPDATE Updates the exp.Parameters object with new value
214+
% Called when either the Condition table or Global param fields are
215+
% updated by the user. Updated the underlying paramters structure
216+
% and notifies listeners of the change via the Changed event.
217+
%
218+
% See also EUI.FIELDPANEL/ONEDIT, EUI.CONDITIONPANEL/ONEDIT
169219
if nargin < 4; row = 1; end
170220
currValue = obj.Parameters.Struct.(name)(:,row);
171221
if iscell(currValue)

tests/ParamEditorTest.m

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
Figure
1010
% Handle to trial conditions UI Table
1111
Table
12+
% Test parameter structure
13+
Parameters
1214
end
1315

14-
properties %(MethodSetupParameter)
15-
Parameters
16+
properties (SetAccess = private)
17+
% Flag set to true each time the ParamEditor's Changed event is
18+
% notified
19+
Changed = false
1620
end
1721

1822
methods (TestClassSetup)
@@ -21,7 +25,7 @@ function setup(testCase)
2125
testCase.FigureVisibleDefault = get(0,'DefaultFigureVisible');
2226
% set(0,'DefaultFigureVisible','off');
2327
testCase.addTeardown(@set, 0, 'DefaultFigureVisible', testCase.FigureVisibleDefault);
24-
28+
2529
% Loads validation data
2630
% Graph data is a cell array where each element is the graph number
2731
% (1:3) and within each element is a cell of X- and Y- axis values
@@ -59,6 +63,13 @@ function buildParams(testCase)
5963
% Ensure all conditional params have column in table
6064
testCase.fatalAssertTrue(isequal(size(testCase.Table.Data), ...
6165
[PE.Parameters.numTrialConditions, numel(PE.Parameters.TrialSpecificNames)]))
66+
% Add callback for verifying that Changed event listeners are
67+
% notified
68+
callback = @(~,~)testCase.setChanged(true);
69+
lh = event.listener(testCase.ParamEditor, 'Changed', callback);
70+
testCase.addTeardown(@delete, lh);
71+
% Reset the Changed flag
72+
testCase.Changed = false;
6273
end
6374
end
6475

@@ -69,6 +80,7 @@ function test_makeConditional(testCase)
6980
% conditional, and that the underlying Parameters object is also
7081
% affected
7182
PE = testCase.ParamEditor;
83+
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
7284
% Number of global parameters: find all text labels
7385
gLabels = @()findobj(testCase.Figure, 'Style', 'text');
7486
gInputs = @()findobj(testCase.Figure, 'Style', 'checkbox', '-or', 'Style', 'edit');
@@ -80,9 +92,12 @@ function test_makeConditional(testCase)
8092
c = findobj(testCase.Figure, 'Text', 'Make Conditional');
8193
% Set the focused object to one of the parameter labels
8294
set(testCase.Figure, 'CurrentObject', ...
83-
findobj(testCase.Figure, 'String', 'rewardVolume'))
95+
findobj(testCase.Figure, 'Tag', 'rewardVolume'))
8496
testCase.verifyWarningFree(c.MenuSelectedFcn, ...
8597
'Problem making parameter conditional');
98+
% Verify Changed event triggered
99+
testCase.verifyTrue(testCase.Changed, ...
100+
'Failed to notify listeners of parameter change')
86101
% Verify change in UI elements
87102
testCase.verifyTrue(numel(gLabels()) == nGlobalLabels-1, ...
88103
'Global parameter UI element not removed')
@@ -152,15 +167,17 @@ function test_paramValue2Control(testCase)
152167

153168
function test_newCondition(testCase)
154169
PE = testCase.ParamEditor;
155-
156170
tableRows = size(testCase.Table.Data, 1);
157171

158172
% Make function handle param conditional to test default value
159173
% Set the focused object to one of the parameter labels
160174
set(testCase.Figure, 'CurrentObject', ...
161-
findobj(testCase.Figure, 'String', 'experimentFun'))
175+
findobj(testCase.Figure, 'Tag', 'experimentFun'))
162176
feval(pick(findobj(testCase.Figure, 'Text', 'Make Conditional'), 'MenuSelectedFcn'))
163177

178+
% Reset Changed flag
179+
testCase.Changed = false;
180+
164181
% Retrieve function handle for new condition
165182
fn = pick(findobj(testCase.Figure, 'String', 'New condition'), 'Callback');
166183
testCase.verifyWarningFree(fn, 'Warning encountered adding trial condition')
@@ -177,11 +194,15 @@ function test_newCondition(testCase)
177194
[~, trialParams] = PE.Parameters.assortForExperiment;
178195
testCase.verifyTrue(isequal(struct2cell(trialParams(end)), ...
179196
{@nop; zeros(2,1); zeros(3,1); false; 0}))
197+
198+
% Verify listeners WEREN'T notified
199+
testCase.verifyTrue(~testCase.Changed, ...
200+
'Shouldn''t have notified listeners of parameter change')
180201
end
181202

182203
function test_deleteCondition(testCase)
183204
PE = testCase.ParamEditor;
184-
205+
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
185206
tableRows = size(testCase.Table.Data, 1);
186207
% Select some cells to delete
187208
event.Indices = [(1:5)' ones(5,1)];
@@ -200,6 +221,10 @@ function test_deleteCondition(testCase)
200221
testCase.assertEqual(size(testCase.Table.Data), ...
201222
[PE.Parameters.numTrialConditions, numel(PE.Parameters.TrialSpecificNames)])
202223

224+
% Verify Changed event triggered
225+
testCase.verifyTrue(testCase.Changed, ...
226+
'Failed to notify listeners of parameter change')
227+
203228
% Test behaviour when < 2 conditions remain
204229
event.Indices = [(1:PE.Parameters.numTrialConditions-1)' ...
205230
ones(PE.Parameters.numTrialConditions-1,1)];
@@ -216,13 +241,14 @@ function test_deleteCondition(testCase)
216241

217242
function test_setValues(testCase)
218243
% TODO Add test for the set values button. For now let's fail this
244+
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
219245
PE = testCase.ParamEditor;
220246
testCase.assertTrue(false, 'Test not implemented')
221247
end
222248

223249
function test_globaliseParam(testCase)
224250
PE = testCase.ParamEditor;
225-
251+
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
226252
tableCols = size(testCase.Table.Data, 2);
227253
% Globalize one param
228254
event.Indices = [1, 2];
@@ -240,6 +266,10 @@ function test_globaliseParam(testCase)
240266
testCase.verifyEqual(size(testCase.Table.Data), ...
241267
[PE.Parameters.numTrialConditions, numel(PE.Parameters.TrialSpecificNames)])
242268

269+
% Verify Changed event triggered
270+
testCase.verifyTrue(testCase.Changed, ...
271+
'Failed to notify listeners of parameter change')
272+
243273
% Test removal of all but numRepeats: numRepeats should automatically
244274
% globalize
245275
n = numel(PE.Parameters.TrialSpecificNames)-1;
@@ -280,6 +310,7 @@ function test_globaliseParam(testCase)
280310
function test_paramEdits(testCase)
281311
% Test basic edits to Global UI controls and Condition table
282312
PE = testCase.ParamEditor;
313+
testCase.assertTrue(~testCase.Changed, 'Changed flag incorrect')
283314

284315
% Retreive all global parameters labels and input controls
285316
gLabels = findobj(testCase.Figure, 'Style', 'text');
@@ -302,6 +333,10 @@ function test_paramEdits(testCase)
302333
par = strcmpi(PE.Parameters.GlobalNames, strrep(gLabels(idx).String, ' ', ''));
303334
testCase.verifyEqual(PE.Parameters.Struct.(PE.Parameters.GlobalNames{par}), 666, ...
304335
'UI edit failed to update parameters struct')
336+
% Verify Changed event triggered
337+
testCase.verifyTrue(testCase.Changed, ...
338+
'Failed to notify listeners of parameter change')
339+
testCase.Changed = false;
305340

306341
% Test editing global param, 'checkbox' UI
307342
idx = find(strcmp({gInputs.Style}, 'checkbox'), 1);
@@ -319,6 +354,10 @@ function test_paramEdits(testCase)
319354
testCase.verifyEqual(...
320355
PE.Parameters.Struct.(PE.Parameters.GlobalNames{par}), gInputs(idx).Value==true, ...
321356
'UI checkbox failed to update parameters struct')
357+
% Verify Changed event triggered
358+
testCase.verifyTrue(testCase.Changed, ...
359+
'Failed to notify listeners of parameter change')
360+
testCase.Changed = false;
322361

323362
% Test edits to conditions table
324363
callback_fcn = testCase.Table.CellEditCallback;
@@ -333,6 +372,9 @@ function test_paramEdits(testCase)
333372
value = PE.Parameters.Struct.(PE.Parameters.TrialSpecificNames{1});
334373
testCase.verifyEqual(value(:,1), [0;5], ...
335374
'Table UI failed to update parameters struct')
375+
% Verify Changed event triggered
376+
testCase.verifyTrue(testCase.Changed, ...
377+
'Failed to notify listeners of parameter change')
336378
end
337379

338380
function test_interactivity(testCase)
@@ -374,4 +416,10 @@ function test_interactivity(testCase)
374416

375417
end
376418

419+
methods
420+
function setChanged(testCase, value)
421+
testCase.Changed = value;
422+
end
423+
end
424+
377425
end

0 commit comments

Comments
 (0)