1
+ classdef ConditionPanel < handle
2
+ % CONDITIONPANEL Deals with formatting trial conditions UI table
3
+ % Designed to be an element of the EUI.PARAMEDITOR class that manages
4
+ % the UI elements associated with all Conditional parameters.
5
+ % TODO Add set condition idx
6
+
7
+ properties
8
+ % Handle to UI Table that represents trial conditions
9
+ ConditionTable
10
+ % Minimum UI Panel width allowed. See also EUI.PARAMEDITOR/ONRESIZE
11
+ MinWidth = 80
12
+ % Handle to parent UI container
13
+ UIPanel
14
+ % Handle to UI container for buttons
15
+ ButtonPanel
16
+ % Handles to context menu items
17
+ ContextMenus
18
+ end
19
+
20
+ properties (Access = protected )
21
+ % Handle to EUI.PARAMEDITOR object
22
+ ParamEditor
23
+ % UIControl button for adding a new trial condition (row) to the table
24
+ NewConditionButton
25
+ % UIControl button for deleting trial conditions (rows) from the table
26
+ DeleteConditionButton
27
+ % UIControl button for making conditional parameter (column) global
28
+ MakeGlobalButton
29
+ % UIControl button for setting multiple table cells at once
30
+ SetValuesButton
31
+ % Indicies of selected table cells as array [row, column;...] of each
32
+ % selected cell
33
+ SelectedCells
34
+ end
35
+
36
+ methods
37
+ function obj = ConditionPanel(f , ParamEditor , varargin )
38
+ % FIELDPANEL Panel UI for Conditional parameters
39
+ % Input f may be a figure or other UI container object
40
+ % ParamEditor is a handle to an eui.ParamEditor object.
41
+ %
42
+ % See also EUI.PARAMEDITOR, EUI.FIELDPANEL
43
+ obj.ParamEditor = ParamEditor ;
44
+ obj.UIPanel = uix .VBox(' Parent' , f );
45
+ % Create a child menu for the uiContextMenus. The input arg is the
46
+ % figure holding the panel
47
+ c = uicontextmenu(ParamEditor .Root );
48
+ obj.UIPanel.UIContextMenu = c ;
49
+ obj.ContextMenus = uimenu(c , ' Label' , ' Make Global' , ...
50
+ ' MenuSelectedFcn' , @(~,~)obj .makeGlobal , ' Enable' , ' off' );
51
+ fcn = @(s ,~)obj .ParamEditor .setRandomized(~strcmp(s .Checked , ' on' ));
52
+ obj .ContextMenus(2 ) = uimenu(c , ' Label' , ' Randomize conditions' , ...
53
+ ' MenuSelectedFcn' , fcn , ' Checked' , ' on' , ' Tag' , ' randomize button' );
54
+ obj .ContextMenus(3 ) = uimenu(c , ' Label' , ' Sort by selected column' , ...
55
+ ' MenuSelectedFcn' , @(~,~)obj .sortByColumn , ...
56
+ ' Tag' , ' sort by' , ' Enable' , ' off' );
57
+ % Create condition table
58
+ p = uix .Panel(' Parent' , obj .UIPanel , ' BorderType' , ' none' );
59
+ obj.ConditionTable = uitable(' Parent' , p ,...
60
+ ' FontName' , ' Consolas' ,...
61
+ ' RowName' , [],...
62
+ ' RearrangeableColumns' , ' on' ,...
63
+ ' Units' , ' normalized' ,...
64
+ ' Position' ,[0 0 1 1 ],...
65
+ ' UIContextMenu' , c ,...
66
+ ' CellEditCallback' , @obj .onEdit ,...
67
+ ' CellSelectionCallback' , @obj .onSelect );
68
+ % Create button panel to hold condition control buttons
69
+ obj.ButtonPanel = uix .HBox(' Parent' , obj .UIPanel );
70
+ % Define some common properties
71
+ props.Style = ' pushbutton' ;
72
+ props.Units = ' normalized' ;
73
+ props.Parent = obj .ButtonPanel ;
74
+ % Create out four buttons
75
+ obj.NewConditionButton = uicontrol(props ,...
76
+ ' String' , ' New condition' ,...
77
+ ' TooltipString' , ' Add a new condition' ,...
78
+ ' Callback' , @(~, ~) obj .newCondition());
79
+ obj.DeleteConditionButton = uicontrol(props ,...
80
+ ' String' , ' Delete condition' ,...
81
+ ' TooltipString' , ' Delete the selected condition' ,...
82
+ ' Enable' , ' off' ,...
83
+ ' Callback' , @(~, ~) obj .deleteSelectedConditions());
84
+ obj.MakeGlobalButton = uicontrol(props ,...
85
+ ' String' , ' Globalise parameter' ,...
86
+ ' TooltipString' , sprintf([' Make the selected condition-specific parameter global (i.e. not vary by trial)\n ' ...
87
+ ' This will move it to the global parameters section' ]),...
88
+ ' Enable' , ' off' ,...
89
+ ' Callback' , @(~, ~) obj .makeGlobal());
90
+ obj.SetValuesButton = uicontrol(props ,...
91
+ ' String' , ' Set values' ,...
92
+ ' TooltipString' , ' Set selected values to specified value, range or function' ,...
93
+ ' Enable' , ' off' ,...
94
+ ' Callback' , @(~, ~) obj .setSelectedValues());
95
+ obj.ButtonPanel.Widths = [-1 - 1 - 1 - 1 ];
96
+ obj.UIPanel.Heights = [-1 25 ];
97
+ end
98
+
99
+ function onEdit(obj , src , eventData )
100
+ % ONEDIT Callback for edits to condition table
101
+ % Updates the underlying parameter struct, changes the UI table
102
+ % data. The src object should be the UI Table that has been edited,
103
+ % and eventData contains the table indices of the edited cell.
104
+ %
105
+ % See also FILLCONDITIONTABLE, EUI.PARAMEDITOR/UPDATE
106
+ row = eventData .Indices(1 );
107
+ col = eventData .Indices(2 );
108
+ assert(all(cellfun(@strcmpi , strrep(obj .ConditionTable .ColumnName , ' ' , ' ' ), ...
109
+ obj .ParamEditor .Parameters .TrialSpecificNames )), ' Unexpected condition names' )
110
+ paramName = obj.ParamEditor.Parameters.TrialSpecificNames{col };
111
+ newValue = obj .ParamEditor .update(paramName , eventData .NewData , row );
112
+ reformed = obj .ParamEditor .paramValue2Control(newValue );
113
+ % If successful update the cell with default formatting
114
+ data = get(src , ' Data' );
115
+ if iscell(reformed )
116
+ % The reformed data type is a cell, this should be a one element
117
+ % wrapping cell
118
+ if numel(reformed ) == 1
119
+ reformed = reformed{1 };
120
+ else
121
+ error(' Cannot handle data reformatted data type' );
122
+ end
123
+ end
124
+ data{row ,col } = reformed ;
125
+ set(src , ' Data' , data );
126
+ end
127
+
128
+ function clear(obj )
129
+ % CLEAR Clear all table data
130
+ % Clears all trial condition data from UI Table
131
+ %
132
+ % See also EUI.PARAMEDITOR/BUILDUI, EUI.PARAMEDITOR/CLEAR
133
+ set(obj .ConditionTable , ' ColumnName' , [], ...
134
+ ' Data' , [], ' ColumnEditable' , false );
135
+ end
136
+
137
+ function delete(obj )
138
+ % DELETE Deletes the UI container
139
+ % Called when this object or its parent ParamEditor is deleted
140
+ % See also CLEAR
141
+ delete(obj .UIPanel );
142
+ end
143
+
144
+ function onSelect(obj , ~, eventData )
145
+ % ONSELECT Callback for when table cells are (de-)selected
146
+ % If at least one cell is selected, ensure buttons and menu items
147
+ % are enabled, otherwise disable them.
148
+ if nargin > 2 ; obj.SelectedCells = eventData .Indices ; end
149
+ controls = ...
150
+ [obj .MakeGlobalButton , ...
151
+ obj .DeleteConditionButton , ...
152
+ obj .SetValuesButton , ...
153
+ obj .ContextMenus([1 ,3 ])];
154
+ set(controls , ' Enable' , iff(size(obj .SelectedCells , 1 ) > 0 , ' on' , ' off' ));
155
+ end
156
+
157
+ function makeGlobal(obj )
158
+ % MAKEGLOBAL Make condition parameter (table column) global
159
+ % Find all selected columns are turn into global parameters, using
160
+ % the value of the first selected cell as the global parameter
161
+ % value.
162
+ %
163
+ % See also eui.ParamEditor/globaliseParamAtCell
164
+ if isempty(obj .SelectedCells )
165
+ disp(' nothing selected' )
166
+ return
167
+ end
168
+ PE = obj .ParamEditor ;
169
+ [cols , iu ] = unique(obj .SelectedCells(: ,2 ));
170
+ names = PE .Parameters .TrialSpecificNames(cols );
171
+ rows = num2cell(obj .SelectedCells(iu ,1 )); % get rows of unique selected cols
172
+ cellfun(@PE .globaliseParamAtCell , names , rows );
173
+ % If only numRepeats remains, globalise it
174
+ if isequal(PE .Parameters .TrialSpecificNames , {' numRepeats' })
175
+ PE .Parameters .Struct .numRepeats(1 ,1 ) = sum(PE .Parameters .Struct .numRepeats );
176
+ PE .globaliseParamAtCell(' numRepeats' , 1 )
177
+ end
178
+ end
179
+
180
+ function deleteSelectedConditions(obj )
181
+ % DELETESELECTEDCONDITIONS Removes the selected conditions from table
182
+ % The callback for the 'Delete condition' button. This removes the
183
+ % selected conditions from the table and if less than two conditions
184
+ % remain, globalizes them.
185
+ %
186
+ % See also EXP.PARAMETERS, GLOBALISESELECTEDPARAMETERS
187
+ rows = unique(obj .SelectedCells(: ,1 ));
188
+ names = obj .ConditionTable .ColumnName ;
189
+ numConditions = size(obj .ConditionTable .Data ,1 );
190
+ % If the number of remaining conditions is 1 or less...
191
+ if numConditions - length(rows ) <= 1
192
+ remainingIdx = find(all(1 : numConditions ~= rows ,1 ));
193
+ if isempty(remainingIdx ); remainingIdx = 1 ; end
194
+ % change selected cells to be all fields (except numRepeats which
195
+ % is assumed to always be the last column)
196
+ obj.SelectedCells = [ones(length(names ),1 )*remainingIdx , (1 : length(names ))' ];
197
+ % ... globalize them
198
+ obj .makeGlobal ;
199
+ else % Otherwise delete the selected conditions as usual
200
+ obj .ParamEditor .Parameters .removeConditions(rows );
201
+ notify(obj .ParamEditor , ' Changed' )
202
+ end
203
+ % Refresh the table of conditions
204
+ obj .fillConditionTable();
205
+ end
206
+
207
+ function setSelectedValues(obj )
208
+ % SETSELECTEDVALUES Set multiple fields in conditional table at once
209
+ % Generates an input dialog for setting multiple trial conditions at
210
+ % once. Also allows the use of function handles for more complex
211
+ % values.
212
+ %
213
+ % Examples:
214
+ % (1:10:100) % Sets selected rows to [1 11 21 31 41 51 61 71 81 91]
215
+ % @(~)randi(100) % Assigned random integer to each selected row
216
+ % @(a)a*50 % Multiplies each condition value by 50
217
+ % false % Sets all selected rows to false
218
+ %
219
+ % See also SETNEWVALS, ONEDIT
220
+ PE = obj .ParamEditor ;
221
+ cols = obj .SelectedCells(: ,2 ); % selected columns
222
+ uCol = unique(obj .SelectedCells(: ,2 ));
223
+ rows = obj .SelectedCells(: ,1 ); % selected rows
224
+ % get current values of selected cells
225
+ currVals = arrayfun(@(u )obj .ConditionTable .Data(rows(cols == u ),u ), uCol , ' UniformOutput' , 0 );
226
+ names = PE .Parameters .TrialSpecificNames(uCol ); % selected column names
227
+ promt = cellfun(@(a ,b ) [a ' (' num2str(sum(cols == b )) ' )' ],...
228
+ names , num2cell(uCol ), ' UniformOutput' , 0 ); % names of columns & num selected rows
229
+ defaultans = cellfun(@(c ) c(1 ), currVals );
230
+ answer = inputdlg(promt ,' Set values' , 1 , cellflat(defaultans )); % prompt for input
231
+ if isempty(answer ) % if user presses cancel
232
+ return
233
+ end
234
+ % set values for each column
235
+ cellfun(@(a ,b ,c ) setNewVals(a ,b ,c ), answer , currVals , names , ' UniformOutput' , 0 );
236
+ function newVals = setNewVals(userIn , currVals , paramName )
237
+ % check array orientation
238
+ currVals = iff(size(currVals ,1 )>size(currVals ,2 ),currVals ' ,currVals );
239
+ if strStartsWith(userIn ,' @' ) % anon function
240
+ func_h = str2func(userIn );
241
+ % apply function to each cell
242
+ currVals = cellfun(@str2double ,currVals , ' UniformOutput' , 0 ); % convert from char
243
+ newVals = cellfun(func_h , currVals , ' UniformOutput' , 0 );
244
+ elseif any(userIn ==' :' ) % array syntax
245
+ arr = eval(userIn );
246
+ newVals = num2cell(arr ); % convert to cell array
247
+ elseif any(userIn ==' ,' |userIn ==' ;' ) % 2D arrays
248
+ C = strsplit(userIn , ' ;' );
249
+ newVals = cellfun(@(c )textscan(c , ' %f ' ,...
250
+ ' ReturnOnError' , false ,...
251
+ ' delimiter' , {' ' , ' ,' }, ' MultipleDelimsAsOne' , 1 ),...
252
+ C );
253
+ else % single value to copy across all cells
254
+ userIn = str2double(userIn );
255
+ newVals = num2cell(ones(size(currVals ))*userIn );
256
+ end
257
+
258
+ if length(newVals )>length(currVals ) % too many new values
259
+ newVals = newVals(1 : length(currVals )); % truncate new array
260
+ elseif length(newVals )<length(currVals ) % too few new values
261
+ % populate as many cells as possible
262
+ newVals = [newVals ...
263
+ cellfun(@(a )PE .controlValue2Param(2 ,a ),...
264
+ currVals(length(newVals )+1 : end ),' UniformOutput' ,0 )];
265
+ end
266
+ ic = strcmp(PE .Parameters .TrialSpecificNames , paramName ); % find edited param names
267
+ % update param struct
268
+ PE .Parameters .Struct.(paramName )(: ,rows(cols == find(ic ))) = cell2mat(newVals );
269
+ % update condtion table with strings
270
+ obj .ConditionTable .Data(rows(cols == find(ic )),ic )...
271
+ = cellfun(@(a )PE .paramValue2Control(a ), newVals ' , ' UniformOutput' , 0 );
272
+ end
273
+ notify(obj .ParamEditor , ' Changed' );
274
+ end
275
+
276
+ function sortByColumn(obj )
277
+ % SORTBYCOLUMN Sort all conditions by selected column
278
+ % If the selected column is already sorted in ascended order then
279
+ % the conditions are ordered in descending order instead.
280
+ % TODO Sort by multiple columns
281
+ % @body currently all conditions are sorted by first selected column
282
+ if isempty(obj .SelectedCells )
283
+ disp(' nothing selected' )
284
+ return
285
+ end
286
+ PE = obj .ParamEditor ;
287
+ % Get selected column name and retrieve data
288
+ cols = unique(obj .SelectedCells(: ,2 ));
289
+ names = PE .Parameters .TrialSpecificNames(cols );
290
+ toSort = PE .Parameters .Struct.(names{1 });
291
+ direction = iff(issorted(toSort ' ,' rows' ), ' descend' , ' ascend' );
292
+ [~ , I ] = sortrows(toSort ' , direction );
293
+ % Update parameters with new permutation
294
+ for p = PE .Parameters .TrialSpecificNames '
295
+ data = PE .Parameters .Struct.(p{: });
296
+ PE .Parameters .Struct.(p{: }) = data(: ,I );
297
+ end
298
+ obj .fillConditionTable % Redraw table
299
+ end
300
+
301
+ function fillConditionTable(obj )
302
+ % FILLCONDITIONTABLE Build the condition table
303
+ % Populates the UI Table with trial specific parameters, where each
304
+ % row is a trial condition (that is, a parameter column) and each
305
+ % column is a different trial specific parameter
306
+ P = obj .ParamEditor .Parameters ;
307
+ titles = P .title(P .TrialSpecificNames );
308
+ [~ , trialParams ] = P .assortForExperiment ;
309
+ if isempty(titles )
310
+ obj.ButtonPanel.Visible = ' off' ;
311
+ obj.UIPanel.Visible = ' off' ;
312
+ obj.ParamEditor.Parent.Widths = [-1 , 1 ];
313
+ else
314
+ obj.ButtonPanel.Visible = ' on' ;
315
+ obj.UIPanel.Visible = ' on' ;
316
+ obj.ParamEditor.Parent.Widths = [-1 , - 1 ];
317
+ end
318
+ data = reshape(struct2cell(trialParams ), numel(titles ), [])' ;
319
+ data = mapToCell(@(e ) obj .ParamEditor .paramValue2Control(e ), data );
320
+ set(obj .ConditionTable , ' ColumnName' , titles , ' Data' , data ,...
321
+ ' ColumnEditable' , true(1 , numel(titles )));
322
+ end
323
+
324
+ function newCondition(obj )
325
+ % Adds a new trial condition (row) to the ConditionTable
326
+ % Adds new row and populates it with sensible 'default' values.
327
+ % These are mostly zeros or empty values.
328
+ % See also eui.ParamEditor/addEmptyConditionToParam
329
+ PE = obj .ParamEditor ;
330
+ cellfun(@PE .addEmptyConditionToParam , ...
331
+ PE .Parameters .TrialSpecificNames );
332
+ obj .fillConditionTable();
333
+ end
334
+
335
+ end
336
+
337
+ end
0 commit comments