% TEXTBOX a textbox object with dynamic textwrapping
%   Can also be inspected with a gui
%   Example:
%     tb = textbox('String','Demonstration textbox')
%     % change values with set
%     set(tb,'String','This works')
%     % or directly change property fields
%     tb.String = 'This also works'
%     % Call inspect method for an interactive gui 
%     tb.inspect()
%
%   Allows dynamic resizing based on content, with or without text
%   wrapping. For dynamic resizing, the position is calculated from the
%   anchor point. If for example the anchor location is NorthEast, this
%   means the upper left corner of the textbox will always be at the anchor
%   position (which is specified relative to the parent figure). If the
%   size is increased, the textbox will expand to the bottom en to the
%   right. 
%
%   See also oop, metaprop

%% Copyright notice
%   --------------------------------------------------------------------
%   Copyright (C) 2014 Van Oord
%       Thijs Damsma
%
%       Thijs.Damsma@VanOord.com
%
%       Schaardijk 211
%       3063 NH
%       Rotterdam
%       Netherlands
%
%   This library is free software: you can redistribute it and/or modify
%   it under the terms of the GNU General Public License as published by
%   the Free Software Foundation, either version 3 of the License, or
%   (at your option) any later version.
%
%   This library is distributed in the hope that it will be useful,
%   but WITHOUT ANY WARRANTY; without even the implied warranty of
%   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%   GNU General Public License for more details.
%
%   You should have received a copy of the GNU General Public License
%   along with this library.  If not, see <http://www.gnu.org/licenses/>.
%   --------------------------------------------------------------------

% This tool is part of <a href="http://www.OpenEarth.eu">OpenEarthTools</a>.
% OpenEarthTools is an online collaboration to share and manage data and
% programming tools in an open source, version controlled environment.
% Sign up to recieve regular updates of this function, and to contribute
% your own tools.

%% Version <http://svnbook.red-bean.com/en/1.5/svn.advanced.props.special.keywords.html>
% Created: 21 Feb 2014
% Created with Matlab version: 8.3.0.73043 (R2014a)

% $Id$
% $Date$
% $Author$
% $Revision$
% $HeadURL$
% $Keywords: $

%%
classdef textbox < oop.inspectable
    % Code template generated by calling metaprop.Template(textbox)
	properties (Constant,Hidden,Transient)
		% Code template generated by calling metaprop.Template(textbox)
		textbox_metaprops = metaprop.Construct(mfilename('class'),{
            'TextWrap',@metaprop.logicalScalar,{
                'Description','Dynamically wrap text inside box'}
            'FitBoxToText',@metaprop.logicalScalar,{
                'Description','Fit the box to the text'}
            'Border',@metaprop.logicalScalar,{
                'Category','Border settings'
                'Description','Draw a border around the textbox'}
            'BorderColor',@metaprop.color,{
                'Category','Border settings'
                'Description','Color of the border around the textbox'}
            'Margin',@metaprop.doubleScalar,{
                'Description','Margin of text with respect to border'}
            'AnchorLocation',@metaprop.stringEnumeration,{
                'Category','Position'
                'Description','The location of the anchor determines the side/corner/center of the textbox of which the position is determined by AnchorPosition'
                'Options',{'Center','North','South','East','West','NorthEast','SouthEast','NorthWest','SouthWest'}}
            'AnchorPosition',@metaprop.doubleRow,{
                'Category','Position'
                'Description','The textbox position is determined by the size and the anchor position'
                'Attributes',{'size', [1 2]}}
            'AnchorVisible',@metaprop.stringEnumeration,{
                'Category','Position'
                'Description','Show or hide the point from which the textbox position is calculated'
                'Options',{'on','off'}}    
            'Size',@metaprop.doubleRow,{
                'Category','Position'
                'Description','The size of the textbox, if FitBoxToText is switched off. If FitBoxToText is switched on, the  determines the '
                'Attributes',{'size', [1 2]}}
            'Position',@metaprop.doubleRow,{
                'Category','Position'
                'Description','Position of the textbox in Units'
                'Attributes',{'size', [1 4]}}
            'BackgroundColor',@metaprop.color,{
                'Description','Background color'}
            'Units',@metaprop.stringEnumeration,{
                'Category','Position'
                'Description','Units in which the width is set'
                'Options',{'inches','centimeters','normalized','points','pixels','characters'}}
            'Visible',@metaprop.stringEnumeration,{
                'Description','Show/hide textbox'
                'Options',{'on','off'}}
            'String',@metaprop.cellstring,{
                'Description','Text in the textbox'
                'Attributes',{}}
            'Font',@metaprop.font,{
                'Category','Font'}                
            'FontColor',@metaprop.color,{
                'Category','Font'}
            'FontAngle',@metaprop.stringEnumeration,{
                'Category','Font'
                'Options',{'normal','italic','oblique'}}
            'FontName',@metaprop.string,{
                'Category','Font'
                'Attributes',{}}
            'FontSize',@metaprop.doubleScalar,{
                'Category','Font'
                'Attributes',{}}
            'FontUnits',@metaprop.stringEnumeration,{
                'Category','Font'
                'Options',{'inches','centimeters','normalized','points','pixels'}}
            'FontWeight',@metaprop.stringEnumeration,{'Category','Font'
                'Options',{'light','normal','demi','bold'}}
            'HorizontalAlignment',@metaprop.stringEnumeration,{
                'Options',{'left','center','right'}}
            'VerticalAlignment',@metaprop.stringEnumeration,{
                'Options',{'top','cap','middle','baseline','bottom'}}
            });
    end
    
    methods (Hidden,Access = protected)
        function value = construct_metaprops(self)
            value = self.textbox_metaprops;
        end
    end
    
    properties
        % settings
        TextWrap = true
        FitBoxToText = false
        Border = true;   
        BorderColor = 'k';
        Margin = 5 
        AnchorLocation = 'Center' % Position of anchor relative to textbox
        %       'Center'             middle center  
        %       'North'              top center
        %       'South'              bottom center
        %       'East'               middle right
        %       'West'               middle left
        %       'NorthEast'          top right
        %       'NorthWest'          top left
        %       'SouthEast'          bottom right
        %       'SouthWest'          bottom left

        AnchorPosition = [.2 .6]; % Position of anchor relative to parent figure
        Size           = [.2 .2]; % Width, Height
    end
    
    properties (SetAccess = protected,Transient)
        ha % handle of box (axes object)
        ht % handle of text inside box (text object)
        hp % handle of anchor point (line object)
    end
    properties (Hidden,SetAccess = protected,Transient)
        ParentPosition
        parentBeingDestroyed
        parentPositionChanged
        screenPixelsPerInch
    end
    
    properties
        String  = ''% stores the string (before wrapping)
    end
    
    properties (Dependent,Hidden,Transient)
        MarginInParentUnits
        MarginInAxesUnits
        TextPosition
        AnchorLocationFactors
    end
    
    properties (Dependent)
        % axes settings
        BackgroundColor % --> ha.Color
        Units % --> ha.Units
        Visible % --> ha.Visible
       
        FontColor % --> ht.Color
        FontAngle % --> ht.FontAngle
        FontName % --> ht.FontName
        FontSize % --> ht.FontSize
        FontUnits % --> ht.FontUnits
        FontWeight % --> ht.FontWeight
        HorizontalAlignment % --> ht.HorizontalAlignment & ht.Position
        VerticalAlignment % --> ht.VerticalAlignment & ht.Position

        AnchorVisible % --> hp.Visible
    end
    
    properties (Dependent,Transient)
        Position % --> self.AnchorPosition, ha.Position
        Parent = [] % --> ha.Parent
        StringWrapped % --> ht.String
        Font % --> all font properties ;
    end
    
    
    methods (Static)
        OutString=textwrap(UIHandle,InString,UIWidth)
    end
    
    %% constructor
    methods
        function self = textbox(varargin)
            % create objects. Many of the defaults 
            self.ha = handle(axes(...
                'parent',gcf,...
                'position',[0.2 0.6 0.2 0.2],...
                'color',[1 1 1],...
                'Box','on',...
                'XTick',[],...
                'YTick',[],...
                'Xlim',[0 1],...
                'Ylim',[0 1],...
                'Units','Normalized',...
                'Visible','on',...
                'XColor','k',...
                'YColor','k'));
            
            self.ht = handle(text(...
                'parent',self.ha,...
                'Position',[0.5 0.5 0],...
                'Color','k',...
                'FontAngle','normal',...
                'FontName','Helvetica',...
                'FontSize',10,...
                'FontUnits','points',...
                'FontWeight','normal',...
                'HorizontalAlignment','center',...
                'VerticalAlignment','middle'));
            
            self.hp = handle(line(...
                'Parent',self.ha,...
                'XData',0.5,...
                'YData',0.5,...
                'ZData',1,...
                'LineWidth',2,...
                'MarkerEdgeColor',[1 0 0],...
                'MarkerFaceColor',[1 0 0],...
                'MarkerSize',6,...
                'Marker','o',...
                'Visible','off'));

            % parse varargins 
            self.set(varargin{:})
            self.updateParentListeners();
        end
    end
    
    %% setters and getters
    methods
        %% property setters
        function set.Border        (self,value); self.metaprops.Border        .Check(value); self.Border         = value; self.update; end
        function set.BorderColor   (self,value); self.metaprops.BorderColor   .Check(value); self.BorderColor    = value; self.update; end
        function set.TextWrap      (self,value); self.metaprops.TextWrap      .Check(value); self.TextWrap       = value; self.update; end
        function set.FitBoxToText  (self,value); self.metaprops.FitBoxToText  .Check(value); self.FitBoxToText   = value; self.update; end
        function set.Margin        (self,value); self.metaprops.Margin        .Check(value); self.Margin         = value; self.update; end
        function set.Size          (self,value); self.metaprops.Size          .Check(value); self.Size           = value; self.update; end
        function set.AnchorPosition(self,value); self.metaprops.AnchorPosition.Check(value); self.AnchorPosition = value; self.update; end
        function set.Position      (self,value); self.metaprops.Position      .Check(value);
            % disable dynamic resizing
            self.FitBoxToText = false;
            
            % calculate new anchor position
            m      = self.AnchorLocationFactors(1);
            n      = self.AnchorLocationFactors(2);
            x      = value(1);
            y      = value(2);
            width  = value(3);
            height = value(4);
            
            self.AnchorPosition = [x + m * width, y + n * height];
            self.Size           = [        width,         height];
        end
        function set.AnchorLocation(self,value); self.metaprops.AnchorLocation.Check(value); self.AnchorLocation = value; self.update; 
            self.updateAnchorHandle(); 
        end
       

        
        %% Dependent axes related property setters
        function set.Parent             (self,value); self.ha.Parent = value; self.updateParentListeners; end
        function set.BackgroundColor    (self,value); self.metaprops.BackgroundColor    .Check(value); self.ha.Color               = value; end
        function set.Units              (self,value); self.metaprops.Units              .Check(value); self.ha.Units               = value; end
        function set.Visible            (self,value); self.metaprops.Visible            .Check(value); self.ha.Visible             = value; self.ht.Visible = value; end
        
        %% Dependent text related property setters
        function set.String             (self,value); self.metaprops.String             .Check(value); self.String                 = value; self.update(); end
        function set.Font               (self,value); self.metaprops.Font               .Check(value);
            self.FontName   = metaprop.font.FontName  (value);
            self.FontSize   = metaprop.font.FontSize  (value);
            self.FontWeight = metaprop.font.FontWeight(value);
            self.FontAngle  = metaprop.font.FontAngle (value);
        end
        function set.FontColor          (self,value); self.metaprops.FontColor          .Check(value); self.ht.Color               = value; self.update(); end
        function set.FontAngle          (self,value); self.metaprops.FontAngle          .Check(value); self.ht.FontAngle           = value; self.update(); end
        function set.FontName           (self,value); self.metaprops.FontName           .Check(value); self.ht.FontName            = value; self.update(); end
        function set.FontSize           (self,value); self.metaprops.FontSize           .Check(value); self.ht.FontSize            = value; self.update(); end
        function set.FontUnits          (self,value); self.metaprops.FontUnits          .Check(value); self.ht.FontUnits           = value; self.update(); end
        function set.FontWeight         (self,value); self.metaprops.FontWeight         .Check(value); self.ht.FontWeight          = value; self.update(); end
        function set.HorizontalAlignment(self,value); self.metaprops.HorizontalAlignment.Check(value); self.ht.HorizontalAlignment = value; self.update(); end
        function set.VerticalAlignment  (self,value); self.metaprops.VerticalAlignment  .Check(value); self.ht.VerticalAlignment   = value; self.update(); end 
        function set.TextPosition       (self,value);                                                  self.ht.Position            = value;                end 
        
        %% Dependent anchor point related property setters
        function set.AnchorVisible      (self,value); self.metaprops.AnchorVisible      .Check(value); self.hp.Visible             = value;                end        
        
       
        %% Dependent axes related property getters
        function value = get.Parent             (self); value = handle(self.ha.Parent); 	 end
        function value = get.BackgroundColor    (self); value = self.ha.Color;               end
        function value = get.Units              (self); value = self.ha.Units; 			     end
        function value = get.Visible            (self); value = self.ha.Visible; 		     end
        function value = get.Position           (self); value = self.ha.Position;	         end
        
        %% Dependent text related property getters
        function value = get.StringWrapped      (self); value = self.ht.String;				 end
        function value = get.Font               (self) 
            isItalic = ~strcmp(self.FontAngle,'normal');
            isBold   = ~strcmp(self.FontWeight,'normal');
            value = java.awt.Font(self.FontName,isBold+2*isItalic,self.FontSize); end
        function value = get.FontColor			(self); value = self.ht.Color;			     end
        function value = get.FontAngle			(self); value = self.ht.FontAngle;			 end
        function value = get.FontName			(self); value = self.ht.FontName;			 end
        function value = get.FontSize			(self); value = self.ht.FontSize;			 end
        function value = get.FontUnits			(self); value = self.ht.FontUnits;			 end
        function value = get.FontWeight			(self); value = self.ht.FontWeight;			 end
        function value = get.HorizontalAlignment(self); value = self.ht.HorizontalAlignment; end
        function value = get.VerticalAlignment	(self); value = self.ht.VerticalAlignment;	 end
        function value = get.TextPosition   	(self); value = self.ht.Position;	         end
        
        %% Dependent anchor point related property getters
        function value = get.AnchorLocationFactors(self)
            switch lower(self.AnchorLocation) % Position of anchor relative to textbox
                case 'center';    value = [0.5 0.5];
                case 'north';     value = [0.5 1.0];
                case 'south';     value = [0.5 0.0];
                case 'east';      value = [1.0 0.5];
                case 'west';      value = [0.0 0.5];
                case 'northeast'; value = [1.0 1.0];
                case 'northwest'; value = [0.0 1.0];
                case 'southeast'; value = [1.0 0.0];
                case 'southwest'; value = [0.0 0.0];
            end
        end
        
        function value = get.AnchorVisible      (self); value = self.hp.Visible;             end 
        
        %% 
        function value = get.MarginInParentUnits(self)
            % helper function to convert margin in pixels to normalized
            % units
            if isempty(self.ha) || isempty(self.ht)
                value = [0 0];
                return
            end
            value = hgconvertunits(self.Parent,[self.Margin self.Margin 0 0],'Pixels',get(self.ha,'Units'),self.Parent);
            value = value([1 2]);
        end
        
        function value = get.MarginInAxesUnits(self)
            value = self.MarginInParentUnits ./ [self.Position(3) self.Position(4)];
        end
    end
    
    methods
        function updateParentListeners(self)
            % clear old listeners
            delete(self.parentBeingDestroyed);
            delete(self.parentPositionChanged);
            delete(self.screenPixelsPerInch);
            
            % set new listeners
            
            if verLessThan('matlab', '8.4')
                self.parentPositionChanged = addlistener(...
                    self.Parent,'Position','PostSet',...
                    @(varargin) self.ParentPositionChanged);
            else
                self.parentPositionChanged = addlistener(...
                    self.Parent,'LocationChanged',...
                    @(varargin) self.ParentPositionChanged);
            end
            self.screenPixelsPerInch = addlistener(0,'ScreenPixelsPerInch',...
                'PostSet',@(varargin) self.ParentPositionChanged);
            
            self.parentBeingDestroyed  = addlistener(self.Parent,'ObjectBeingDestroyed',@(varargin) self.delete);

            % store position
            self.ParentPosition = [get(self.Parent,'Position') get(0,'ScreenPixelsPerInch')];
            
            % update
            self.update;
        end
    
        function updateAnchorHandle(self)
            self.hp.XData = self.AnchorLocationFactors(1);
            self.hp.YData = self.AnchorLocationFactors(2);
        end
        
        function ParentPositionChanged(self)
            self.update();
            
            % the code below was supposed to prevent updates when nothing was
            % changed, but as there is a two step process in updating a
            % figure position, (first update numbers, run callback one,
            % then update firgure on screen, and callback second time) 
            % the first time number change, but on screen nothing is
            % different, so e.g. no wrapping, but second time figure
            % properties (numbers) do not appear to have been changed, but
            % on screen in fact they are, so there is no reliable way to
            % know is recalculating wrapping is neccessary. 
            % Solution: always recalculate
            
            % newParentPosition = [get(self.Parent,'Position') get(0,'ScreenPixelsPerInch')];
            % if ~isequal(...
            %       newParentPosition ([3 4 5]),...
            %       self.ParentPosition([3 4 5]));
            % 
            %   self.ParentPosition = newParentPosition;
            % end
        end
        
        %% core methods
        function position = calculatePosition(self,alternativeSize)
            if nargin==1
                % alternativeSize = optional
                width  = self.Size(1);
                height = self.Size(2);
            else
                width  = alternativeSize(1);
                height = alternativeSize(2);
            end
            m      = self.AnchorLocationFactors(1);
            n      = self.AnchorLocationFactors(2);

            xa     = self.AnchorPosition(1);
            ya     = self.AnchorPosition(2);
            x      = xa - m * width;
            y      = ya - n * height;
            
            position = [x y width height];
        end
        
        function update(self)
            %% exception for when printing
            st = dbstack;
            if any(strcmp('hardcopy',{st.name}))
                % disable updates when printing, because the wrap function
                % can not detect the needed wrapping due to settings changed by
                % the print function (and this function is p code...)
                % also messes up margins
                return
            end

            if isempty(self.ha) || isempty(self.ht)
                return
            end
            
            if self.Border
                self.ha.Box = 'on';
                self.ha.XColor   = self.BorderColor;
                self.ha.YColor   = self.BorderColor;
            else
                self.ha.Box = 'off';
                self.ha.XColor   = self.BackgroundColor;
                self.ha.YColor   = self.BackgroundColor;
            end

            self.ha.Position = self.calculatePosition();
            
            % set string and wrap it
            self.ht.String = self.String;
            if self.TextWrap
                maxwidth       = 1 - self.MarginInAxesUnits(1)*2;
                if self.ht.Extent(3) > maxwidth
                    % text is too wide, wrap it
                    self.ht.String = self.textwrap(self.ht,self.String,maxwidth);
                end
            end
            
            if self.FitBoxToText
                width  = (self.ha.Position(3) * self.ht.Extent(3)) + self.MarginInParentUnits(1)*2;
                height = (self.ha.Position(4) * self.ht.Extent(4)) + self.MarginInParentUnits(2)*2;
                if self.TextWrap
                    width  = self.Size(1);
                    height = max(height,self.Size(2));
                end
                self.ha.Position = self.calculatePosition([width height]);
            end
            
            switch self.HorizontalAlignment
                case 'right';  self.TextPosition(1) = 1 - self.MarginInAxesUnits(1);
                case 'center'; self.TextPosition(1) = 0.5;
                case 'left';   self.TextPosition(1) = 0 + self.MarginInAxesUnits(1);
            end
            switch self.VerticalAlignment
                case 'top';    self.TextPosition(2) = 1 - self.MarginInAxesUnits(2);
                case 'middle'; self.TextPosition(2) = 0.5;
                case 'bottom'; self.TextPosition(2) = 0 + self.MarginInAxesUnits(2);
            end
        end
        
        function delete(self)
            delete(self.parentBeingDestroyed)
            delete(self.parentPositionChanged)
            delete(self.screenPixelsPerInch)
            
            if ishghandle(self.ha)
                delete(self.ha);
            end
            if ishghandle(self.ht)
                delete(self.ht);
            end
            
            % call superclass delete method
            delete@oop.inspectable(self);
        end
    end
end