diff --git a/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYd.xml b/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYd.xml new file mode 100644 index 0000000..80b5b16 --- /dev/null +++ b/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYp.xml b/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYp.xml new file mode 100644 index 0000000..720ce9b --- /dev/null +++ b/resources/project/FhYM8VeT-dlS07YkW6aAPpMEDOc/6e44kSUWEpBnPnuEC0rb4FlnrlYp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cd.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cd.xml new file mode 100644 index 0000000..80b5b16 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cp.xml b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cp.xml new file mode 100644 index 0000000..1034750 --- /dev/null +++ b/resources/project/elO4D6tOV7Lp-Jfo7ptgr2NlB30/wAwe9VOvfujTcIcVeNy0Q57c44cp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYd.xml b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYd.xml new file mode 100644 index 0000000..80b5b16 --- /dev/null +++ b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYp.xml b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYp.xml new file mode 100644 index 0000000..576d57c --- /dev/null +++ b/resources/project/f1CasPtPWEKkULO6lQ_WTjau2Nw/FYJ4_1h7UhnnazGirpjMb0ul4QYp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/widgets/+wt/+model/ThumbnailCache.m b/widgets/+wt/+model/ThumbnailCache.m new file mode 100644 index 0000000..5630fc6 --- /dev/null +++ b/widgets/+wt/+model/ThumbnailCache.m @@ -0,0 +1,518 @@ +classdef ThumbnailCache + % Manages a cache of image thumbnails given a folder or file list + + %% Public properties + properties (AbortSet) + + % List of files that get converted to thumbnails + SourceFiles (:,1) string {mustBeFile} + + % Thumbnail size + DefSize (1,1) double {mustBePositive,mustBeFinite} = 200 + end + + %% Calculated properties + properties (Dependent = true) + HighlightColor % Color of the highlight around the selected image + end % Calculated properties + + %% Private properties + properties (SetAccess='private', GetAccess='private') + + % Indicates if each thumbnail is loaded from cache + IsLoaded (1,:) logical + + %indicates if each thumbnail is queued as a future + IsQueued (1,:) logical + + ThumbCacheFile char = ''; + + ThumbCacheMap (1,1) struct = struct(); + + % Timer for background scanning of thumbnails + ThumbTimer + + % parallel futures for thumbnail creation + ThumbFutures + + TimerPeriod (1,1) double {mustBePositive,mustBeFinite} = 0.2; %TimerPeriod + + ImagesPerBatch (1,1) double {mustBePositive,mustBeFinite} = 10; %ImagesPerBatch + + UseParallel(1,1) logical = false; %UseParallel + + end % Private properties + + + %% Constant properties + properties (Constant, GetAccess='private') + PngReadPath = fullfile(matlabroot,'toolbox','matlab','imagesci','private'); + end % Constant properties + + + %% Constructor / Destructor + methods + + function obj = ThumbnailCache(root) + + % Define arguments + arguments + root (1,1) string = "" + end + + % Prep thumbnail caching + obj.initiateThumbCache(); + + % Start timer to update thumbnails + start(obj.ThumbTimer); + + end % constructor + + + function delete(obj) + % Destroy the ThumbTimer + if ~isempty(obj.ThumbTimer) && isvalid(obj.ThumbTimer) + stop(obj.ThumbTimer); + delete(obj.ThumbTimer); + end + + % Store the thumbnail map cache file + thumbMap = obj.ThumbCacheMap; + save( obj.ThumbCacheFile, '-struct', 'thumbMap' ); + end + + end %constructor/destructor methods + + + %% Protected methods + methods (Access=protected) + + function initiateThumbCache(obj) + + % Define the thumbnail cache + filename = sprintf('ImageSelectorThumbnails_%d.mat',obj.DefSize); + obj.ThumbCacheFile = fullfile( tempdir(), filename ); + + % Grab the thumbnail map cache file, if one exists + MakeNewMap = true; + if exist( obj.ThumbCacheFile, 'file' ) == 2 + thumbMap = load( obj.ThumbCacheFile ); + if isstruct(thumbMap) &&... + isfield(thumbMap,'ThumbFile') && isa(thumbMap.ThumbFile,'containers.Map') &&... + isfield(thumbMap,'Size') && isa(thumbMap.Size,'containers.Map') &&... + isfield(thumbMap,'Date') && isa(thumbMap.Size,'containers.Map') + obj.ThumbCacheMap = thumbMap; + MakeNewMap = false; + end + end + if MakeNewMap + obj.ThumbCacheMap = struct(... + 'ThumbFile', containers.Map('KeyType','char','ValueType','char'),... + 'Date', containers.Map('KeyType','char','ValueType','double'),... + 'Size', containers.Map('KeyType','char','ValueType','double') ); + end + + % Create the thumbnail timer, which scans for thumbnails as a + % periodic background task + obj.ThumbTimer = timer(... + 'Name', 'ImageSelectorThumbnailUpdateTimer', ... + 'ExecutionMode', 'fixedSpacing', ... + 'BusyMode', 'Drop', ... + 'TimerFcn', @(t,s)getThumbnails(obj,t,s), ... + 'ObjectVisibility', 'off', ... + 'StartDelay', obj.TimerPeriod, ... + 'Period', obj.TimerPeriod); + %'ErrorFcn', @(h,e)assignin('base','errInfo',e),... + + end + + + function getThumbnails( obj, thisTimer, ~ ) + + % Check whether construction is complete and there are + % remaining thumbnails to load + if obj.IsConstructed && all(obj.IsLoaded) + + % We can stop the timer now - everything is loaded + stop(thisTimer); + + elseif obj.IsConstructed + + % First, check for any PCT futures that completed + if ~isempty(obj.ThumbFutures) + + IsComplete = strcmp({obj.ThumbFutures.State}, 'finished'); + idxToFetch = find(IsComplete); + + for idx = 1:numel(idxToFetch) + + % Get the result + [thumbFileName, cdata, srcFileName] = obj.ThumbFutures(idxToFetch(idx)).fetchOutputs(); + + % Match up the index of this image + idxThisImage = strcmp( obj.SourceFiles, srcFileName ); + + % Update the thumbnail map + if ~isempty(thumbFileName) + obj.ThumbCacheMap.ThumbFile(srcFileName) = thumbFileName; + obj.IsLoaded(idxThisImage) = true; + obj.IsQueued(idxThisImage) = false; + end + + % Store original CData in appdata (for enable/disable) + setappdata( obj.h.Images(idxThisImage), 'CData', cdata ); + + % If widget disabled, move color towards background + if strcmpi( obj.Enable, 'off' ) + bgcol = obj.BackgroundColor; + cdata(:,:,1) = 0.5*bgcol(1) + 0.5*cdata(:,:,1); + cdata(:,:,2) = 0.5*bgcol(2) + 0.5*cdata(:,:,2); + cdata(:,:,3) = 0.5*bgcol(3) + 0.5*cdata(:,:,3); + end + + % Update the image cdata to display it + set( obj.h.Images(idxThisImage), 'CData', cdata ); + + end %for idx = 1:numel(idxToFetch) + + % Remove these futures + delete( obj.ThumbFutures(IsComplete) ); + obj.ThumbFutures(IsComplete) = []; + + end %if ~isempty(obj.ThumbFutures) + + % Which next N number of thumbnails should be loaded? + NumThisBatch = obj.ImagesPerBatch - sum(obj.IsQueued); + if NumThisBatch <= 0 + return + end + CheckToLoad = ~obj.IsLoaded & ~obj.IsQueued; + idxToLoad = find(CheckToLoad, NumThisBatch); + + % Quickest way to load existing PNG thumbnails is to call the + % mex file pngreadc directly. But we need to cd to the private + % folder to be able to call it. + currentDir = pwd; + cd(obj.PngReadPath); + + % Are any of these thumbnails cached already? + IsCached = false(size(idxToLoad)); + for idx = 1:numel(idxToLoad) + + ii = idxToLoad(idx); + srcFileName = obj.SourceFiles{ii}; + + % Confirm date and size + fInfo = dir(srcFileName); + if isscalar(fInfo) + srcFileSize = fInfo.bytes; + srcFileDate = fInfo.datenum; + else + warning('Unable to scan thumbnail image: %s',srcFileName); + srcFileSize = 0; + srcFileDate = 0; + end + + % Is it cached? + IsCached(idx) = ( ... + obj.ThumbCacheMap.ThumbFile.isKey(srcFileName) &&... + exist(obj.ThumbCacheMap.ThumbFile(srcFileName),'file')==2 &&... + obj.ThumbCacheMap.Size(srcFileName) == srcFileSize &&... + obj.ThumbCacheMap.Date(srcFileName) == srcFileDate ); + + % Depending on cache, we load or create it + if IsCached(idx) + + % Load an existing thumbnail + thumbname = obj.ThumbCacheMap.ThumbFile(obj.SourceFiles{ii}); + + % Load the thumbnail cache. Use internal mex file pngreadc + % for speed, since we know the format. + cdata = pngreadc(thumbname, [], false); + cdata = permute(cdata, ndims(cdata):-1:1); + obj.IsLoaded(ii) = true; + + % Store original CData in appdata (for enable/disable) + setappdata( obj.h.Images(ii), 'CData', cdata ); + + % If widget disabled, move color towards background + if strcmpi( obj.Enable, 'off' ) + bgcol = obj.BackgroundColor; + cdata(:,:,1) = 0.5*bgcol(1) + 0.5*cdata(:,:,1); + cdata(:,:,2) = 0.5*bgcol(2) + 0.5*cdata(:,:,2); + cdata(:,:,3) = 0.5*bgcol(3) + 0.5*cdata(:,:,3); + end + + % Update the image cdata to display it + set( obj.h.Images(ii), 'CData', cdata ); + + else + % If not cached yet, we will cache it but we also + % need to store the source file's size and date to + % check if it changes later + obj.ThumbCacheMap.Size(srcFileName) = srcFileSize; + obj.ThumbCacheMap.Date(srcFileName) = srcFileDate; + + end %if IsCached(idx) + + end %for ii = idxToLoad + + % Navigate back to the user's current directory + cd(currentDir); + + % Do we need to create thumbnails from this set? + if any(~IsCached) + + % Which ones should be created? + idxToCreate = idxToLoad(~IsCached); + + % Can we use PCT? + if obj.UseParallel + + % Use parfeval to create thumbnails as background tasks + for idx = 1:numel(idxToCreate) + srcFileName = obj.SourceFiles{idxToCreate(idx)}; + if isempty(obj.ThumbFutures) + obj.ThumbFutures = parfeval(@uiw.utility.createThumbnail, 3, srcFileName, obj.DefSize); + else + obj.ThumbFutures(end+1) = parfeval(@uiw.utility.createThumbnail, 3, srcFileName, obj.DefSize); + end + end + + % Mark them as queued in a future + obj.IsQueued(idxToCreate) = true; + + else + + % NO - create just one thumbnail now in this timer loop + idx = 1; + ii = idxToCreate(idx); + srcFileName = obj.SourceFiles{idxToCreate(idx)}; + + % Create the thumbnail + try + [thumbFileName, cdata] = uiw.utility.createThumbnail( srcFileName, obj.DefSize ); + catch err + obj.IsLoaded(ii) = true; + warning('ImageSelector:createThumbnailError',... + 'Unable to create thumbnail for ''%s''. Error: %s',... + srcFileName, err.message); + return + end + + % Update the thumbnail map + if ~isempty(thumbFileName) && isscalar(fInfo) + obj.ThumbCacheMap.ThumbFile(srcFileName) = thumbFileName; + obj.IsLoaded(ii) = true; + end + + % Store original CData in appdata (for enable/disable) + setappdata( obj.h.Images(ii), 'CData', cdata ); + + % If widget disabled, move color towards background + if strcmpi( obj.Enable, 'off' ) + bgcol = obj.BackgroundColor; + cdata(:,:,1) = 0.5*bgcol(1) + 0.5*cdata(:,:,1); + cdata(:,:,2) = 0.5*bgcol(2) + 0.5*cdata(:,:,2); + cdata(:,:,3) = 0.5*bgcol(3) + 0.5*cdata(:,:,3); + end + + % Update the image cdata to display it + set( obj.h.Images(ii), 'CData', cdata ); + + end %if obj.UseParallel + + end %if any(~IsCached) + + end %if obj.IsConstructed && ~all(obj.IsLoaded) + + end % getThumbnails + + end % Protected methods + + + + %% Private methods + methods (Access='private') + + + + function addImage( obj, filename, caption ) + %addImage: add a new image to the list + % + % obj.addImage(FILENAME,CAPTION) + if nargin<3 + caption = repmat("",size(filename)); + end + numAdds = numel(filename); + + % For blank thumbnails + cdata(obj.DefSize, obj.DefSize, 3) = uint8(0); + + % Note the new indices that will need thumbnails + idxThumbnails = numel(obj.h.Images) + (1:numAdds); + + % Get the state of files and captions + imageFiles = obj.SourceFiles; + captions = obj.Captions; + + % Append items to lists, and create the UI components + for ii=1:numAdds + + % Get the index of the next image + idx = idxThumbnails(ii); + + % Append the image and caption to the list, if not already + % done + if numel(imageFiles) + obj.IsLoaded(toremove) = []; %#ok + obj.IsQueued(toremove) = []; %#ok + + % Now add the new ones + toadd = ~iStrIsMember( names, obj.SourceFiles ); + obj.SourceFiles = horzcat(obj.SourceFiles, names(toadd)); + obj.addImage( names(toadd) ); + + end % set.SourceFiles + + + end % Data access methods + + +end % classdef + + + +%% Now some helper functions + +%-------------------------------------------------------------------------% +function tf = iStrIsMember( strsToLookFor, strSet ) +% This is faster than ismember +n = numel(strsToLookFor); +tf = false(size(strsToLookFor)); +for idx=1:n + tf(idx) = any(strcmp(strsToLookFor{idx},strSet)); +end +end %iStrIsMember + +%-------------------------------------------------------------------------% +function tf = iIsEqualMember( itemsToLookFor, fullSet ) +% This is faster than ismember +n = numel(itemsToLookFor); +tf = false(size(itemsToLookFor)); +for idx=1:n + tf(idx) = any(itemsToLookFor(idx) == fullSet); +end +end %iIsEqualMember \ No newline at end of file diff --git a/widgets/+wt/ImageGallery.m b/widgets/+wt/ImageGallery.m new file mode 100644 index 0000000..2e4d417 --- /dev/null +++ b/widgets/+wt/ImageGallery.m @@ -0,0 +1,193 @@ +classdef ImageGallery < wt.abstract.BaseWidget + % A gallery of images + + % Copyright 2020-2021 The MathWorks Inc. + + %RAJ - to do: + % verify performance with bigger files + % Do we need thumbnail generation?? + % Enable setting a folder instead of a file list + % Enable a file type filter in this case + + %% Public properties + properties (AbortSet) + + % Size of the image space in pixels + ImageSize (1,1) double = 200 + + % Image file sources + ImageSource (:,1) string = [""; ""; ""] + + end %properties + + + %% Internal Properties + properties ( Transient, NonCopyable, ... + Access = {?wt.abstract.BaseWidget, ?wt.test.BaseWidgetTest} ) + + % Image controls + Image (1,:) matlab.ui.control.Image + + % Size changed listener + SizeChangedListener event.listener + + end %properties + + + properties (AbortSet, Transient, NonCopyable, ... + Access = {?wt.abstract.BaseWidget, ?wt.test.BaseWidgetTest} ) + + % Number of visible [rows columns] + GridSize (1,2) double = [1 3] + + end %properties + + + + %% Protected methods + methods (Access = protected) + + function setup(obj) + + % Call superclass setup first to establish the grid + obj.setup@wt.abstract.BaseWidget(); + + % Set default size + obj.Position(3:4) = [400 400]; + obj.Grid.Padding = [0 0 0 0]; + + % Turn on scrollability + obj.Grid.Scrollable = true; + + % Listen to resize events + obj.SizeChangedListener = event.listener(obj,"SizeChanged",... + @(src,evt)obj.onSizeChanged(evt) ); + + end %function + + + function update(obj) + + % How many images are needed? + numImg = numel(obj.ImageSource); + oldNum = numel(obj.Image); + + % Check and update grid size as needed + obj.updateGridSize(); + + % Calculate layout position of each image + cIdx = repmat(1:obj.GridSize(2), obj.GridSize(1), 1)'; + rIdx = repmat(1:obj.GridSize(1), obj.GridSize(2), 1); + + % Add new images if needed + if numImg > oldNum + for idx = oldNum+1:numImg + obj.Image(idx) = uiimage(obj.Grid, "ScaleMethod", "fill"); + obj.Image(idx).Layout.Row = rIdx(idx); + obj.Image(idx).Layout.Column = cIdx(idx); + end + end %if + + % Update each image source + for idx = 1:numImg + wt.utility.fastSet(obj.Image(idx),... + "ImageSource", obj.ImageSource(idx)); + end %for + + % Delete any extra images + delete( obj.Image(numImg+1:end) ) + obj.Image(numImg+1:end) = []; + + end %function + + end %methods + + + + %% Private methods + methods %(Access = private) + + function onSizeChanged(obj,~) + % Triggered on size changed + + % Check and update grid size as needed + %obj.updateGridSize(); + obj.requestUpdate(); + + end %function + + + function updateGridSize(obj) + % Calculate and update the grid size + + % How many images? + numImg = numel(obj.ImageSource); + + % How much space do we have? + if obj.Units == "pixels" + wAvail = obj.Position(3) + obj.Grid.ColumnSpacing; + else + pos = getpixelposition(obj.Grid); + wAvail = (pos(3) + obj.Grid.ColumnSpacing); + end + + % How many images do we need to fit? + wPerImg = obj.ImageSize + obj.Grid.ColumnSpacing; + + % How many full columns fit across? + numCol = max(1, floor(wAvail/wPerImg)); + + % How many rows do we need? (Rows can be scrolled down) + numRow = max(1, ceil(numImg/numCol)); + + % Update the layout size + obj.GridSize = [numRow numCol]; + + end %function + + + function updateLayout(obj) + % Update the layout based on the current GridSize + % This should only be called by set.GridSize + + % How many images? + numImg = min( prod(obj.GridSize), numel(obj.Image) ); + + % Make the layout updates + colWidth = repmat({obj.ImageSize}, 1, obj.GridSize(2)); + rowHeight = repmat({obj.ImageSize}, 1, obj.GridSize(1)); + + % Calculate layout position of each image + rIdx = repmat(1:obj.GridSize(1), obj.GridSize(2), 1); + cIdx = repmat(1:obj.GridSize(2), obj.GridSize(1), 1)'; + + % Update layout position of each image + for idx = 1:numImg + obj.Image(idx).Layout.Row = rIdx(idx); + if obj.Image(idx).Layout.Column ~= cIdx(idx) + obj.Image(idx).Layout.Column = cIdx(idx); + end + end %for + + % Update the grid layout sizes + obj.Grid.RowHeight = rowHeight; + obj.Grid.ColumnWidth = colWidth; + + end %function + + + end %methods + + + %% Accessors + methods + + function set.GridSize(obj,value) + obj.GridSize = value; + obj.updateLayout(); + end + + end % methods + +end % classdef + diff --git a/widgets/examples/rjImageGallery.m b/widgets/examples/rjImageGallery.m new file mode 100644 index 0000000..d58f5bf --- /dev/null +++ b/widgets/examples/rjImageGallery.m @@ -0,0 +1,20 @@ +%% Create the image gallery +f = uifigure; +g = uigridlayout(f,[1,1]); +w = ImageGallery(g,'BackgroundColor','green') + + +%% Show some images from MATLAB +searchRoot = fullfile(matlabroot,'toolbox','images','imdata'); +fileInfo = dir( fullfile(searchRoot,"*.png") ); +fileNames = sortrows( string({fileInfo.name}') ); +filePaths = fullfile(searchRoot, fileNames); +w.ImageSource = filePaths; + + +%% Big images +searchRoot = "C:\Users\rjackey\OneDrive - MathWorks\Pictures\2011-10-31 Argentina\"; +fileInfo = dir( fullfile(searchRoot,"*.jpg") ); +fileNames = sortrows( string({fileInfo.name}') ); +filePaths = fullfile(searchRoot, fileNames); +w.ImageSource = filePaths;