classdef Grid < uiextras.Container
    %Grid  Container with contents arranged in a grid
    %
    %   obj = uiextras.Grid() creates a new new grid layout with all
    %   properties set to defaults. The number of rows and columns to use
    %   is determined from the number of elements in the RowSizes and
    %   ColumnSizes properties respectively. Child elements are arranged
    %   down column one first, then column two etc. If there are
    %   insufficient columns then a new one is added. The output is a new
    %   layout object that can be used as the parent for other
    %   user-interface components. The output is a new layout object that
    %   can be used as the parent for other user-interface components.
    %
    %   obj = uiextras.Grid(param,value,...) also sets one or more
    %   parameter values.
    %
    %   See the <a href="matlab:doc uiextras.Grid">documentation</a> for more detail and the list of properties.
    %
    %   Examples:
    %   >> f = figure();
    %   >> g = uiextras.Grid( 'Parent', f, 'Spacing', 5 );
    %   >> uicontrol( 'Style', 'frame', 'Parent', g, 'Background', 'r' )
    %   >> uicontrol( 'Style', 'frame', 'Parent', g, 'Background', 'b' )
    %   >> uicontrol( 'Style', 'frame', 'Parent', g, 'Background', 'g' )
    %   >> uiextras.Empty( 'Parent', g )
    %   >> uicontrol( 'Style', 'frame', 'Parent', g, 'Background', 'c' )
    %   >> uicontrol( 'Style', 'frame', 'Parent', g, 'Background', 'y' )
    %   >> set( g, 'ColumnSizes', [-1 100 -2], 'RowSizes', [-1 100] );
    %
    %   See also: uiextras.GridFlex
    
    %   Copyright 2009-2010 The MathWorks, Inc.
    %   1.1 
    %   2012/05/08 08:02:59
    
    properties( SetObservable ) 
        Padding = 0 % padding around and between contents [pixels]
        Spacing = 0 % spacing between contents [pixels]
    end % public properties
    
    properties( Dependent )
        RowSizes           % column vector of row sizes, with positive elements for absolute sizes (pixels) and negative elements for relative sizes
        ColumnSizes        % column vector of column sizes, with positive elements for absolute sizes (pixels) and negative elements for relative sizes
        MinimumRowSizes    % minimum size (in pixels) for each row
        MinimumColumnSizes % minimum size (in pixels) for each column
    end % dependent properties
    
    properties( Access=private )
  
        
    end % private properties
    properties( Access = private )
        RowSizes_ = zeros( 0, 1 )           % private property for storing actual row sizes
        ColumnSizes_ = zeros( 0, 1 )        % private property for storing actual column sizes
        MinimumRowSizes_ = zeros( 0, 1 )    % private property for storing actual minimum row sizes
        MinimumColumnSizes_ = zeros( 0, 1 ) % private property for storing actual minimum column sizes
    end % private properties
    
    methods
        
        function obj = Grid( varargin )
            %Grid  Create a container with contents in a grid.
            
            % First step is to create the parent class. We pass the
            % arguments (if any) just incase the parent needs setting
            obj@uiextras.Container( varargin{:} );

            % Set some defaults
            obj.setPropertyFromDefault( 'Padding' );
            obj.setPropertyFromDefault( 'Spacing' );

            % Set user-supplied property values (only if this is the leaf class)
            if nargin>0 && isequal( class( obj ), 'uiextras.Grid' )
                set( obj, varargin{:} );
            end
            
        end % constructor
        
    end % public methods
    
    methods
        
        function set.RowSizes( obj, value )
            
            % Check
            if ~isvector( value )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''RowSizes'' must be a vector.' )
            elseif ~isnumeric( value ) || ...
                    any( ~isreal( value ) ) || any( isnan( value ) ) || any( ~isfinite( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''RowSizes'' must consist of real, finite, numeric values.' )
            end
            
            % Set
            obj.RowSizes_ = value(:);
            
            % Make sure min row sizes is the same length
            nMinSizes = length(obj.MinimumRowSizes_);
            nSizes = length(obj.RowSizes_);
            if nMinSizes > nSizes
                obj.MinimumRowSizes_ = obj.MinimumRowSizes_(1:nSizes);
            elseif nMinSizes < nSizes
                obj.MinimumRowSizes_(end+1:nSizes,1) = 1;
            end
            
            % Add/remove elements to/from ColumnSizes, if required
            nColumns = ceil( numel( obj.Children ) / numel( obj.RowSizes_ ) );
            if numel( obj.ColumnSizes_ ) > nColumns
                obj.ColumnSizes_(nColumns+1:end,:) = [];
                obj.MinimumColumnSizes_(nColumns+1:end,:) = [];
            elseif numel( obj.ColumnSizes ) < nColumns
                obj.ColumnSizes_(end+1:nColumns,1) = -1;
                obj.MinimumColumnSizes_(end+1:nColumns,1) = 1;
            end
            
            % Redraw
            obj.redraw();
            
        end % set.RowSizes
        
        function value = get.RowSizes( obj )
            value = obj.RowSizes_;
        end % get.RowSizes
        
        function set.ColumnSizes( obj, value )
            % Check
            if ~isvector( value )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''ColumnSizes'' must be a vector.' )
            elseif ~isnumeric( value ) || ...
                    any( ~isreal( value ) ) || any( isnan( value ) ) || any( ~isfinite( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''ColumnSizes'' must consist of real, finite, numeric values.' )
            end
            
            % Set
            obj.ColumnSizes_ = value(:);
            
            % Make sure min row sizes is the same length
            nMinSizes = length(obj.MinimumColumnSizes_);
            nSizes = length(obj.ColumnSizes_);
            if nMinSizes > nSizes
                obj.MinimumColumnSizes_ = obj.MinimumColumnSizes_(1:nSizes);
            elseif nMinSizes < nSizes
                obj.MinimumColumnSizes_(end+1:nSizes,1) = 1;
            end
            
            % Add/remove elements to/from RowSizes, if required
            nRows = ceil( numel( obj.Children ) / numel( obj.ColumnSizes_ ) );
            if numel( obj.RowSizes_ ) > nRows
                obj.RowSizes_(nRows+1:end,:) = [];
                obj.MinimumRowSizes_(nRows+1:end,:) = [];
            elseif numel( obj.RowSizes ) < nRows
                obj.RowSizes_(end+1:nRows,1) = -1;
                obj.MinimumRowSizes_(end+1:nRows,1) = 1;
            end
            
            % Redraw
            obj.redraw();
        end % set.ColumnSizes
        
        function value = get.ColumnSizes( obj )
            value = obj.ColumnSizes_;
        end % get.ColumnSizes       
                
        function set.MinimumRowSizes( obj, value )
            % Check that this vector matches the size vector
            if ~isequal( numel( obj.RowSizes ), numel( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Size of property ''MinimumRowSizes'' must match size of property ''RowSizes''.' )
            elseif ~isnumeric( value ) || ...
                    any( ~isreal( value ) ) || any( isnan( value ) ) || any( ~isfinite( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''MinimumRowSizes'' must consist of real, finite, numeric values.' )
            end
            
            % Set and redraw
            obj.MinimumRowSizes_ = value(:)';
            obj.redraw();
        end % set.MinimumRowSizes
        
        function value = get.MinimumRowSizes( obj )
            value = obj.MinimumRowSizes_;
        end % get.MinimumRowSizes
                
        function set.MinimumColumnSizes( obj, value )
            % Check that this vector matches the size vector
            if ~isequal( numel( obj.ColumnSizes ), numel( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Size of property ''MinimumColumnSizes'' must match size of property ''RowSizes''.' )
            elseif ~isnumeric( value ) || ...
                    any( ~isreal( value ) ) || any( isnan( value ) ) || any( ~isfinite( value ) )
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''MinimumColumnSizes'' must consist of real, finite, numeric values.' )
            end
            
            % Set and redraw
            obj.MinimumColumnSizes_ = value(:)';
            obj.redraw();
        end % set.MinimumColumnSizes
        
        function value = get.MinimumColumnSizes( obj )
            value = obj.MinimumColumnSizes_;
        end % get.MinimumColumnSizes
        
        function set.Padding( obj, value )
            % Check
            if ~isnumeric( value ) || ~isscalar( value ) || ...
                    ~isreal( value ) || isnan( value ) || ~isfinite( value ) || ...
                    value < 0 || rem( value, 1 ) ~= 0
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''Padding'' must be a nonnegative integer.' )
            end
            
            % Set
            obj.Padding = value;
            
            % Redraw
            obj.redraw();
        end % set.Padding
        
        function set.Spacing( obj, value )
            % Check
            if ~isnumeric( value ) || ~isscalar( value ) || ...
                    ~isreal( value ) || isnan( value ) || ~isfinite( value ) || ...
                    value < 0 || rem( value, 1 ) ~= 0
                error( 'GUILayout:InvalidPropertyValue', ...
                    'Property ''Spacing'' must be a nonnegative integer.' )
            end
            
            % Set
            obj.Spacing = value;
            
            % Redraw
            obj.redraw();
        end % set.Spacing
        
    end % accessor methods
    
    methods( Access = protected )
        
        function [widths,heights] = redraw( obj )
            %REDRAW  Redraw container contents.
            
            % Get container width and height
            totalPosition = ceil( getpixelposition( obj.UIContainer ) );
            totalWidth = totalPosition(3);
            totalHeight = totalPosition(4);
            
            % Get children
            children = obj.Children;
            nChildren = numel( children );
            
            % Get padding, spacing, widths and heights
            padding = obj.Padding;
            spacing = obj.Spacing;
            columnSizes = obj.ColumnSizes;
            minColumnSizes = obj.MinimumColumnSizes;
            rowSizes = obj.RowSizes;
            minRowSizes = obj.MinimumRowSizes;
            nColumns = numel( columnSizes );
            nRows = numel( rowSizes );
            
            % Compute widths and heights
            widths = uiextras.calculatePixelSizes( totalWidth, ...
                columnSizes, minColumnSizes, ...
                obj.Padding, obj.Spacing );
            heights = uiextras.calculatePixelSizes( totalHeight, ...
                rowSizes, minRowSizes, ...
                obj.Padding, obj.Spacing );
            
            % Compute and set new positions in pixels
            elementNumbers = reshape( 1:nRows*nColumns, [nRows, nColumns] );
            rowNumbers = repmat( transpose( 1:nRows ), [1, nColumns] );
            columnNumbers = repmat( 1:nColumns, [nRows, 1] );
            for ii = 1:nChildren
                child = children(ii);
                jj = rowNumbers(elementNumbers==ii); % row index
                kk = columnNumbers(elementNumbers==ii); % column index
                x = sum( widths(1:kk-1) ) + padding + spacing * (kk-1) + 1;
                y = totalHeight - sum( heights(1:jj) ) - padding - spacing*(jj-1) + 1;
                newPosition = [x, y, widths(kk), heights(jj)];   
                obj.repositionChild( child, newPosition );
            end
            
        end % redraw
        
        function onChildAdded( obj, source, eventData ) %#ok<INUSD>
            %onChildAdded: Callback that fires when a child is added to a container.
            % Add element to RowSizes, if required
            if numel( obj.RowSizes_ ) == 0
                obj.RowSizes_ = -1;
                obj.MinimumRowSizes_ = 1;
            end
            
            % Add element to ColumnSizes, if required
            nColumns = ceil( numel( obj.Children ) / numel( obj.RowSizes_ ) );
            if numel( obj.ColumnSizes_ ) < nColumns
                obj.ColumnSizes_(end+1:nColumns,:) = -1;
                obj.MinimumColumnSizes_(end+1:nColumns,:) = 1;
            end
            
            obj.redraw();
            
        end % onChildAdded
        
        function onChildRemoved( obj, source, eventData ) %#ok<INUSD>
            %onChildAdded: Callback that fires when a container child is destroyed or reparented.
            if numel( obj.Children ) == 0
                % Remove elements from RowSizes and ColumnSizes
                obj.RowSizes_ = zeros( 0, 1 );
                obj.ColumnSizes_ = zeros( 0, 1 );
                obj.MinimumRowSizes_ = zeros( 0, 1 );
                obj.MinimumColumnSizes_ = zeros( 0, 1 );
            else
                % Remove elements from ColumnSizes, if required
                nColumns = ceil( numel( obj.Children ) / numel( obj.RowSizes_ ) );
                if numel( obj.ColumnSizes_ ) > nColumns
                    obj.ColumnSizes_(nColumns+1:end,:) = [];
                    obj.MinimumColumnSizes_(nColumns+1:end,:) = [];
                end
            end
            
            % Redraw
            obj.redraw();
        end % onChildRemoved
        
    end % protected methods
    
end % classdef