diff --git a/DeepSqueak.fig b/DeepSqueak.fig index 3e465e84..5d64db3a 100644 Binary files a/DeepSqueak.fig and b/DeepSqueak.fig differ diff --git a/DeepSqueak.m b/DeepSqueak.m index 9d6fcb8e..54ff44bd 100644 --- a/DeepSqueak.m +++ b/DeepSqueak.m @@ -282,6 +282,46 @@ function PreviousCall_Callback(hObject, eventdata, handles) update_fig(hObject, eventdata, handles); +% --- Executes on button press in NextFile. +function NextFile_Callback(hObject, eventdata, handles) +numfiles = length(handles.detectionfiles); +%Make sure we actually have an active Detections folder +if numfiles > 0 + handles.current_file_id = get(handles.popupmenuDetectionFiles,'Value'); + % Check for a next file + if handles.current_file_id < numfiles + % If confirmed possible, increment to next file + handles.current_file_id = handles.current_file_id + 1; + filename = fullfile(handles.detectionfiles(handles.current_file_id).folder, handles.detectionfiles(handles.current_file_id).name); + % filename argument bypasses the behavior of pressing the LoadCalls button + cancelled = loadcalls_Callback(hObject, eventdata, handles, 'filename', filename); + % Make sure the drop-down matches what's happening internally + if ~cancelled + handles.popupmenuDetectionFiles.Value = handles.current_file_id; + end + end +end + +% --- Executes on button press in PrevFile. +function PrevFile_Callback(hObject, eventdata, handles) +numfiles = length(handles.detectionfiles); +%Make sure we actually have an active Detections folder +if numfiles > 0 + handles.current_file_id = get(handles.popupmenuDetectionFiles,'Value'); + % Check for a previous file + if handles.current_file_id > 1 + % If confirmed possible, decrement file + handles.current_file_id = handles.current_file_id - 1; + filename = fullfile(handles.detectionfiles(handles.current_file_id).folder, handles.detectionfiles(handles.current_file_id).name); + % filename argument bypasses the behavior of pressing the LoadCalls button + cancelled = loadcalls_Callback(hObject, eventdata, handles, 'filename', filename); + % Make sure the drop-down matches what's happening internally + if ~cancelled + handles.popupmenuDetectionFiles.Value = handles.current_file_id; + end + end +end + % --- Executes on selection change in Networks Folder Pop up. function neuralnetworkspopup_Callback(hObject, eventdata, handles) guidata(hObject, handles); @@ -494,11 +534,11 @@ function export_raven_Callback(hObject, eventdata, handles) end end a = cell2table(raventable); -handles.current_file_id = get(handles.popupmenuDetectionFiles,'Value'); -current_detection_file = handles.detectionfiles(handles.current_file_id).name; -ravenname=[strtok(current_detection_file,'.') '_Raven.txt']; -[FileName,PathName] = uiputfile(ravenname,'Save Raven Truth Table (.txt)'); -writetable(a,[PathName FileName],'delimiter','\t','WriteVariableNames',false); +% Get the name of the output file and save the table +[PathName, FileName, ~] = fileparts(handles.current_detection_file); +ravenName = fullfile(PathName, [FileName '_Raven.txt']); +[FileName,PathName] = uiputfile(ravenName,'Save Raven Truth Table (.txt)'); +writetable(a, fullfile(PathName, FileName), 'delimiter', '\t', 'WriteVariableNames', false); guidata(hObject, handles); % -------------------------------------------------------------------- @@ -697,12 +737,22 @@ function backwardButton_Callback(hObject, eventdata, handles) jumps = floor(handles.data.focusCenter / handles.data.settings.pageSize); handles.data.windowposition = jumps*handles.data.settings.pageSize; -calls_within_window = find(handles.data.calls.Box(:,1) < handles.data.windowposition + handles.data.settings.pageSize, 1, 'last'); -if ~isempty(calls_within_window) - handles.data.currentcall = calls_within_window; - handles.data.current_call_valid = true; +% if we can't page because we're at the beg of the file, make the first call +% the current call +if jumps == 0 + if ~isempty(handles.data.calls) + handles.data.currentcall = 1; + handles.data.current_call_valid = true; + end +% Otherwise make the last call in the new page window the current call +% (will not necessarily be in focusWindow) +else + calls_within_window = find(handles.data.calls.Box(:,1) < handles.data.windowposition + handles.data.settings.pageSize, 1, 'last'); + if ~isempty(calls_within_window) + handles.data.currentcall = calls_within_window; + handles.data.current_call_valid = true; + end end - update_fig(hObject, eventdata, handles); @@ -718,10 +768,21 @@ function forwardButton_Callback(hObject, eventdata, handles) % handles.data.focusCenter = max(0, handles.data.windowposition - handles.data.settings.focus_window_size ./ 2); % get_closest_call_to_focus(hObject, eventdata, handles); -calls_within_window = find(handles.data.calls.Box(:,1) > handles.data.windowposition, 1); -if ~isempty(calls_within_window) - handles.data.currentcall = calls_within_window; - handles.data.current_call_valid = true; +% if we can't page because we're at the end of the file, make the last call +% the current call +if jumps == 0 + if ~isempty(handles.data.calls) + handles.data.currentcall = height(handles.data.calls); + handles.data.current_call_valid = true; + end +% Otherwise make the first call in the new page window the current call +% (will not necessarily be in focusWindow) +else + calls_within_window = find(handles.data.calls.Box(:,1) > handles.data.windowposition, 1); + if ~isempty(calls_within_window) + handles.data.currentcall = calls_within_window; + handles.data.current_call_valid = true; + end end update_fig(hObject, eventdata, handles); @@ -820,30 +881,32 @@ function topRightButton_Callback(hObject, eventdata, handles) % --- Executes on button press in loadAudioFile. function loadAudioFile_Callback(hObject, eventdata, handles) +cancelled = checkForUnsavedChanges(hObject, eventdata, handles); +if cancelled + return +end + h = waitbar(0,'Loading Audio Please wait...'); update_folders(hObject, eventdata, handles); handles = guidata(hObject); -if nargin == 3 % if "Load Calls" button pressed - if isempty(handles.audiofiles) - close(h); - errordlg(['No valid audio files in current audio folder. Select a folder containing audio with '... - '"File -> Select Audio Folder", then choose the desired file in the "Audio Files" dropdown box.']) - return - end - handles.current_file_id = get(handles.AudioFilespopup,'Value'); - handles.current_audio_file = handles.audiofiles(handles.current_file_id).name; +if isempty(handles.audiofiles) + close(h); + errordlg(['No valid audio files in current audio folder. Select a folder containing audio with '... + '"File -> Select Audio Folder", then choose the desired file in the "Audio Files" dropdown box.']) + return end -handles.data.audiodata = audioinfo(fullfile(handles.data.settings.audiofolder,handles.current_audio_file)); +current_file_id = get(handles.AudioFilespopup,'Value'); +handles.data.audiodata = audioinfo(fullfile(handles.data.settings.audiofolder, handles.audiofiles(current_file_id).name)); + +% Create a filename for the new detection file +[~, filename, ~] = fileparts(handles.data.audiodata.Filename); +handles.current_detection_file = fullfile(handles.data.settings.detectionfolder, [filename '.mat']); + +% Initialize the empty calls variable +handles.data.calls = table(zeros(0,4),[],[],[], 'VariableNames', {'Box', 'Score', 'Type', 'Accept'}); -Calls = table(zeros(0,4),[],[],[], 'VariableNames', {'Box', 'Score', 'Type', 'Accept'}); -% Calls.Box = [0 0 1 1]; -% Calls.Score = 0; -% Calls.Type = categorical({'NA'}); -% Calls.Power = 1; -% Calls.Accept = false; -handles.data.calls = Calls; % Position of the focus window handles.data.focusCenter = handles.data.settings.focus_window_size ./ 2; initialize_display(hObject, eventdata, handles); diff --git a/Functions/Batch_Reject_by_Threshold_Callback.m b/Functions/Batch_Reject_by_Threshold_Callback.m index 127d11e6..142103b1 100644 --- a/Functions/Batch_Reject_by_Threshold_Callback.m +++ b/Functions/Batch_Reject_by_Threshold_Callback.m @@ -157,7 +157,7 @@ function Batch_Reject_by_Threshold_Callback(hObject, eventdata, handles) end close(h); -%update the display -if isfield(handles,'current_detection_file') && any(ismember(handles.detectionfilesnames(selections),handles.current_detection_file)) - loadcalls_Callback(hObject, eventdata, handles, handles.current_file_id) +% reload the file if the currently loaded file was modified +if isfield(handles,'current_detection_file') && any(ismember(fullfile(handles.detectionfiles(1).folder,handles.detectionfilesnames(selections)),handles.current_detection_file)) + loadcalls_Callback(hObject, eventdata, handles, 'filename', handles.current_detection_file, 'checkForChanges', false) end diff --git a/Functions/checkForUnsavedChanges.m b/Functions/checkForUnsavedChanges.m new file mode 100644 index 00000000..179c03f0 --- /dev/null +++ b/Functions/checkForUnsavedChanges.m @@ -0,0 +1,28 @@ +function cancelled = checkForUnsavedChanges(hObject, eventdata, handles) +%% Check if the current file has been modified. If it has, prompt the user to save it +% cancelled = false if the user pressed "Yes" or "No" +% cancelled = true if user pressed "Cancel" + +cancelled = false; +if ~isempty(handles.data.calls) + if isfile(handles.current_detection_file) + oldCalls = loadCallfile(handles.current_detection_file, handles); + % Check if the "box", "type", and "accept" fields in the currently loaded + % file are the same as the old file. Do this instead of comparing + % all fields because previous versions of DS had additional + % variables, so this prevents compatibility errors. + % isequal doesn't work when categorical variables have undefined + % entries, so test them seperately. + if ~isequal(handles.data.calls(:,{'Box', 'Accept'}), oldCalls(:,{'Box', 'Accept'})) || ~isequal(cellstr(handles.data.calls.Type), cellstr(oldCalls.Type)) + response = questdlg('Calls have been modified, save changes?', 'Save changes', 'Yes', 'No', 'Cancel', 'Yes'); + switch response + case 'Yes' + cancelled = savesession_Callback(hObject, eventdata, handles); + case 'No' + cancelled = false; + case 'Cancel' + cancelled = true; + end + end + end +end \ No newline at end of file diff --git a/Functions/loadCallfile.m b/Functions/loadCallfile.m index 91815ff6..e1458f49 100644 --- a/Functions/loadCallfile.m +++ b/Functions/loadCallfile.m @@ -9,6 +9,10 @@ %% Unpack the data if isfield(data, 'Calls') Calls = data.Calls; + % Back Compatability for Files with Power in The Detection File. + if ismember('Power',Calls.Properties.VariableNames) + Calls = removevars(Calls, 'Power'); + end elseif nargout < 3 % If ClusteringData is requested, we don't need Calls error('This doesn''t appear to be a detection file!') end diff --git a/Functions/loadcalls_Callback.m b/Functions/loadcalls_Callback.m index 4c3e626e..7b6b8575 100644 --- a/Functions/loadcalls_Callback.m +++ b/Functions/loadcalls_Callback.m @@ -1,9 +1,29 @@ % --- Executes on button press in LOAD CALLS. -function loadcalls_Callback(hObject, eventdata, handles, reload_current_file) +function cancelled = loadcalls_Callback(hObject, eventdata, handles, varargin) +% Inputs: +% filename - full path of the file to load. If filename is not given, load the file selected in the dropdown menu. +% checkForChanges - If true, check if the current session matches the saved file +% Outputs +% cancelled - true if the user pressed cancel when prompted to save changes, else false + +p = inputParser; +addParameter(p, 'filename', []); +addParameter(p, 'checkForChanges', true); +parse(p, varargin{:}); + + +cancelled = false; +if p.Results.checkForChanges + cancelled = checkForUnsavedChanges(hObject, eventdata, handles); +end +if cancelled + return +end + h = waitbar(0,'Loading Calls Please wait...'); update_folders(hObject, eventdata, handles); handles = guidata(hObject); -if nargin == 3 % if "Load Calls" button pressed, load the selected file, else reload the current file +if isempty(p.Results.filename) % if "Load Calls" button pressed, load the selected file, else reload the current file if isempty(handles.detectionfiles) close(h); errordlg(['No valid detection files in current folder. Select a folder containing detection files with '... @@ -11,17 +31,14 @@ function loadcalls_Callback(hObject, eventdata, handles, reload_current_file) return end handles.current_file_id = get(handles.popupmenuDetectionFiles,'Value'); - handles.current_detection_file = handles.detectionfiles(handles.current_file_id).name; + handles.current_detection_file = fullfile(handles.detectionfiles(handles.current_file_id).folder, handles.detectionfiles(handles.current_file_id).name); +elseif isfile(p.Results.filename) + handles.current_detection_file = p.Results.filename; end handles.data.calls = []; handles.data.audiodata = []; -[handles.data.calls, handles.data.audiodata] = loadCallfile(fullfile(handles.detectionfiles(handles.current_file_id).folder, handles.current_detection_file), handles); - -%% Back Compatability for Files with Power in The Detection File. -if ismember('Power',handles.data.calls.Properties.VariableNames) - handles.data.calls = removevars(handles.data.calls,'Power'); -end +[handles.data.calls, handles.data.audiodata] = loadCallfile(handles.current_detection_file, handles); % Position of the focus window to the first call in the file handles.data.focusCenter = handles.data.calls.Box(1,1) + handles.data.calls.Box(1,3)/2; diff --git a/Functions/savesession_Callback.m b/Functions/savesession_Callback.m index 552414a6..f4256adc 100644 --- a/Functions/savesession_Callback.m +++ b/Functions/savesession_Callback.m @@ -1,28 +1,22 @@ -function savesession_Callback(hObject, eventdata, handles) +function cancelled = savesession_Callback(hObject, eventdata, handles) +% cancelled = false if saved successfully, else success = true +cancelled = false; -handles.v_det = get(handles. popupmenuDetectionFiles,'Value'); -if isfield(handles,'current_detection_file') - handles.SaveFile = handles.detectionfiles(handles.v_det).name; - handles.SaveFile = handles.current_detection_file; -else - handles.SaveFile = [strtok(handles.audiofiles(handles.v_det).name,'.') '.mat']; +if isempty(handles.data.calls) + disp('Can''t save session, no calls are loaded') + return end -% temp = handles.data.audiodata.samples; -% handles.data.audiodata.samples = []; -guidata(hObject, handles); - Calls = handles.data.calls; audiodata = handles.data.audiodata; -[FileName, PathName] = uiputfile(fullfile(handles.data.settings.detectionfolder, handles.SaveFile), 'Save Session (.mat)'); +[FileName, PathName] = uiputfile(handles.current_detection_file, 'Save Session (.mat)'); if FileName == 0 + cancelled = true; return end -h = waitbar(0.5, 'saving'); - -save(fullfile(PathName, FileName), 'Calls','audiodata', '-v7.3'); -% handles.data.audiodata.samples = temp; +h = waitbar(0.5, 'saving'); +save(fullfile(PathName, FileName), 'Calls','audiodata', '-append'); update_folders(hObject, eventdata, handles); -guidata(hObject, handles); close(h); + diff --git a/Functions/set_static_box_height_Callback.m b/Functions/set_static_box_height_Callback.m index 3bfaf870..3076f0ba 100644 --- a/Functions/set_static_box_height_Callback.m +++ b/Functions/set_static_box_height_Callback.m @@ -54,6 +54,6 @@ function set_static_box_height_Callback(hObject, eventdata, handles) guidata(hObject, handles); %% Update display -if isfield(handles,'current_detection_file') && any(contains(fname, handles.current_detection_file)) - loadcalls_Callback(hObject, eventdata, handles, true) +if isfield(handles,'current_detection_file') && any(ismember(fullfile(fpath, fname), handles.current_detection_file)) + loadcalls_Callback(hObject, eventdata, handles, 'filename', handles.current_detection_file, 'checkForChanges', false) end