Skip to content

Commit cb45e03

Browse files
fangqclaude
andcommitted
[neuroj] show what the GUI is doing while an operation blocks
A watch-cursor change was the only feedback during loads, which on a slow connection looked identical to a hung window. Every blocking operation now announces itself: - a centred message panel inside the figure (not a separate window, so it is guaranteed to paint and dies with the browser), - the same message in the status bar on a highlighted background, - all four lists, the toolbar and the context menu greyed out, so a second operation cannot be started on top of the first, - the watch pointer, as before. Messages name the work rather than saying "please wait": "Downloading cotilab/NeuroCaptain_2025...", "Listing datasets in openneuro...", "Scanning "Atlas_Age_19_0" for linked files...". The slowest path, loading a document, reports its two phases separately (download, then building the tree). setbusy() takes a message string, but setbusy(h, true) still works. A blocking call cannot animate itself, since MATLAB and Octave both run callbacks on the main thread and neither offers a timer usable here (Octave has no timer at all). The spinner therefore advances one frame per reported phase, which is the only honest motion available; it is not a busy loop pretending to be one. Clearing the busy state restores per-item enabling through updateactions() rather than switching everything back on, so a Preview button that was correctly disabled for an uncached link stays disabled afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 462b939 commit cb45e03

1 file changed

Lines changed: 82 additions & 18 deletions

File tree

neuroj.m

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@
198198

199199
handles = buildmenus(handles);
200200

201+
% centred overlay shown while a blocking operation runs; a panel inside the
202+
% figure rather than a separate window, so it is guaranteed to paint and is
203+
% destroyed together with the browser
204+
handles.pnBusy = uipanel(handles.fmMain, 'units', 'normalized', 'position', [0.30 0.44 0.40 0.12], ...
205+
'visible', 'off', 'backgroundcolor', [1 0.95 0.75]);
206+
handles.txBusy = uicontrol(handles.pnBusy, 'style', 'text', 'units', 'normalized', ...
207+
'position', [0.04 0.1 0.92 0.8], 'backgroundcolor', [1 0.95 0.75], ...
208+
'fontweight', 'bold', 'HorizontalAlignment', 'center', 'string', '');
209+
201210
set(handles.fmMain, 'userdata', handles);
202211
set(handles.fmMain, 'WindowButtonDownFcn', @(src, events) splitterdown(handles.fmMain));
203212
set(handles.fmMain, 'WindowButtonUpFcn', @(src, events) splitterup(handles.fmMain));
@@ -962,20 +971,73 @@ function setenable(h, tf)
962971
end
963972

964973
% --------------------------------------------------------------------------
965-
function setbusy(hwin, isbusy)
966-
% show progress by switching the figure pointer - unlike the previously used
967-
% hidden msgbox, this is destroyed together with the browser window
974+
function setbusy(hwin, state)
975+
% make a blocking operation visible: grey out the lists and tools so a second
976+
% one cannot be started, show a centred message panel and a watch pointer.
977+
%
978+
% state is false to clear, true for a generic wait, or a message string.
979+
% MATLAB and Octave both run callbacks on the main thread, so a blocking call
980+
% cannot animate itself - the indicator is painted before the call starts and
981+
% callers advance it between steps with setbusy(hwin, 'next phase...').
968982

969983
if (~ishandle(hwin))
970984
return
971985
end
972-
if (isbusy)
986+
handles = get(hwin, 'userdata');
987+
if (~isstruct(handles) || ~isfield(handles, 'pnBusy'))
988+
return
989+
end
990+
991+
busy = ~(isequal(state, false) || isequal(state, 0));
992+
lists = [handles.lsDb, handles.lsDs, handles.lsJSON, handles.lsPeek];
993+
994+
if (busy)
995+
message = 'Working...';
996+
if (ischar(state) && ~isempty(state))
997+
message = state;
998+
end
999+
set(handles.txBusy, 'string', [spinchar(hwin) ' ' message]);
1000+
set(handles.pnBusy, 'visible', 'on');
1001+
set(handles.txStatus, 'string', message, 'backgroundcolor', [1 0.95 0.75], 'fontweight', 'bold');
9731002
set(hwin, 'pointer', 'watch');
1003+
set(lists, 'enable', 'off');
1004+
settoolenable(handles, false);
9741005
else
1006+
set(handles.pnBusy, 'visible', 'off');
1007+
set(handles.txStatus, 'backgroundcolor', get(hwin, 'color'), 'fontweight', 'normal');
9751008
set(hwin, 'pointer', 'arrow');
1009+
set(lists, 'enable', 'on');
1010+
updateactions(hwin);
9761011
end
9771012
drawnow;
9781013

1014+
% --------------------------------------------------------------------------
1015+
function ch = spinchar(hwin)
1016+
% advance a one-character spinner; it turns whenever a caller reports a new
1017+
% phase, which is the only motion achievable while the main thread is blocked
1018+
1019+
frames = '|/-\';
1020+
idx = getappdata(hwin, 'spinstep');
1021+
if (isempty(idx))
1022+
idx = 0;
1023+
end
1024+
idx = mod(idx, length(frames)) + 1;
1025+
setappdata(hwin, 'spinstep', idx);
1026+
ch = frames(idx);
1027+
1028+
% --------------------------------------------------------------------------
1029+
function settoolenable(handles, tf)
1030+
% toggle every toolbar button and context-menu entry at once; the contextual
1031+
% per-item state is restored afterwards by updateactions()
1032+
1033+
names = {'btDownload', 'btPreview', 'btSaveAs', 'btSubtree', 'btAttach', ...
1034+
'miDownload', 'miPreview', 'miSaveAs', 'miSubtree', 'miAttach'};
1035+
for i = 1:length(names)
1036+
if (isfield(handles, names{i}))
1037+
setenable(handles.(names{i}), tf);
1038+
end
1039+
end
1040+
9791041
% --------------------------------------------------------------------------
9801042
function tf = isactivated(handles, event)
9811043
% a list entry is "activated" by a double-click or by pressing enter; the
@@ -1830,7 +1892,7 @@ function peekselect(src, events, hwin)
18301892
peekkeys = getappdata(hwin, 'peekkeys');
18311893
idx = get(handles.lsPeek, 'value');
18321894
if (node.ok && ~isempty(peekkeys) && idx(1) >= 1 && idx(1) <= length(peekkeys))
1833-
setbusy(hwin, true);
1895+
setbusy(hwin, 'Opening...');
18341896
try
18351897
descendinto(hwin, node.key);
18361898
selectkey(hwin, peekkeys{idx(1)});
@@ -2088,7 +2150,7 @@ function actdownload(hwin)
20882150
return
20892151
end
20902152

2091-
setbusy(hwin, true);
2153+
setbusy(hwin, sprintf('Downloading %s...', node.label));
20922154
cachedfile = '';
20932155
try
20942156
[~, cachedfile] = jdlink(node.meta.url);
@@ -2115,14 +2177,15 @@ function actpreview(hwin)
21152177
return
21162178
end
21172179

2118-
setbusy(hwin, true);
2180+
setbusy(hwin, sprintf('Decoding %s for preview...', node.label));
21192181
try
21202182
data = resolvenode(node);
21212183
catch err
21222184
setbusy(hwin, false);
21232185
errordlg(['Cannot decode the selected data: ' err.message], 'Preview');
21242186
return
21252187
end
2188+
setbusy(hwin, 'Rendering preview...');
21262189
setbusy(hwin, false);
21272190
try
21282191
previewdata(data, node.label);
@@ -2156,7 +2219,7 @@ function actsaveas(hwin)
21562219
end
21572220
target = fullfile(fpath, fname);
21582221

2159-
setbusy(hwin, true);
2222+
setbusy(hwin, sprintf('Saving %s...', fname));
21602223
try
21612224
if (strcmp(node.meta.kind, 'datalink') && ~isempty(node.meta.cachefile))
21622225
copyfile(node.meta.cachefile, target);
@@ -2237,7 +2300,7 @@ function actdownloadattachments(hwin)
22372300
return
22382301
end
22392302

2240-
setbusy(hwin, true);
2303+
setbusy(hwin, sprintf('Scanning "%s" for linked files...', label));
22412304
links = subtreelinks(data);
22422305
setbusy(hwin, false);
22432306
if (isempty(links))
@@ -2354,7 +2417,7 @@ function actexportsubtree(hwin)
23542417
end
23552418
target = fullfile(fpath, fname);
23562419

2357-
setbusy(hwin, true);
2420+
setbusy(hwin, sprintf('Exporting subtree to %s...', fname));
23582421
try
23592422
writesubtree(data, target);
23602423
catch err
@@ -2745,7 +2808,7 @@ function exportdataset(hwin)
27452808
return
27462809
end
27472810

2748-
setbusy(hwin, true);
2811+
setbusy(hwin, sprintf('Exporting %s/%s to a folder...', dbname, dsname));
27492812
try
27502813
res = neuroj('export', dbname, dsname);
27512814
catch err
@@ -2764,7 +2827,7 @@ function exportdataset(hwin)
27642827
function loaddb(src, event, hwin)
27652828

27662829
handles = get(hwin, 'userdata');
2767-
setbusy(hwin, true);
2830+
setbusy(hwin, 'Listing databases on neurojson.io...');
27682831
try
27692832
dbs = neuroj('list');
27702833
dbids = cellfun(@(x) x.id, dbs.database, 'UniformOutput', false);
@@ -2844,7 +2907,7 @@ function dosearch(hwin)
28442907

28452908
param = [param, 'limit', strtrim(get(handles.hLimit, 'string')), ...
28462909
'skip', strtrim(get(handles.hSkip, 'string'))];
2847-
setbusy(hwin, true);
2910+
setbusy(hwin, 'Searching neurojson.io...');
28482911
try
28492912
result = webread(baseurl, param{:});
28502913
catch err
@@ -3018,7 +3081,7 @@ function loadds(src, event, hwin)
30183081
handles = get(hwin, 'userdata');
30193082
if (isactivated(handles, event))
30203083
dbname = selecteditem(hwin, handles.lsDb, 'dbkeys');
3021-
setbusy(hwin, true);
3084+
setbusy(hwin, sprintf('Listing datasets in %s...', dbname));
30223085
try
30233086
searchdatasets = getappdata(hwin, 'searchDatasets');
30243087
if (~isempty(searchdatasets) && isa(searchdatasets, 'containers.Map') && isKey(searchdatasets, dbname))
@@ -3052,7 +3115,7 @@ function loaddsdata(src, event, hwin)
30523115
setstatus(hwin, 'Please select a database and a dataset first');
30533116
return
30543117
end
3055-
setbusy(hwin, true);
3118+
setbusy(hwin, sprintf('Loading %s/%s...', dbname, dsname));
30563119
try
30573120
% a search result lists the matching subjects instead of the document tree
30583121
searchsubjects = getappdata(hwin, 'searchSubjects');
@@ -3087,9 +3150,10 @@ function loaddataset(hwin, dbid, dsname)
30873150
% load a document without decoding JData constructs, so the browser can show
30883151
% the _DataLink_/_ArrayType_ metadata and decode only what the user asks for
30893152

3090-
handles = get(hwin, 'userdata');
3153+
setbusy(hwin, sprintf('Downloading %s/%s...', dbid, dsname));
30913154
data = neuroj('get', dbid, dsname, '', 'jdatadecode', 0);
30923155

3156+
setbusy(hwin, sprintf('Building the tree for %s...', dsname));
30933157
setappdata(hwin, 'rootdata', data);
30943158
setappdata(hwin, 'pathstack', {});
30953159
setappdata(hwin, 'dbname', dbid);
@@ -3110,7 +3174,7 @@ function expandjsontree(src, event, hwin)
31103174
if (~activated)
31113175
shownodeinfo(hwin);
31123176
else
3113-
setbusy(hwin, true);
3177+
setbusy(hwin, 'Opening...');
31143178
try
31153179
stepintonode(hwin);
31163180
catch err
@@ -3119,7 +3183,7 @@ function expandjsontree(src, event, hwin)
31193183
setbusy(hwin, false);
31203184
end
31213185
elseif (activated && ~isempty(getappdata(hwin, 'subjectrows')))
3122-
setbusy(hwin, true);
3186+
setbusy(hwin, 'Loading the dataset for this subject...');
31233187
try
31243188
opensubject(hwin);
31253189
catch err

0 commit comments

Comments
 (0)