classdef DialogTester < hgsetget % handle
    %App for developing and testing Dialog Manager dialogs.
    %
    % Use:
    %   t = dialogmgr.DialogTest(Dialog)
    %   t = dialogmgr.DialogTest([Dialog1 Dialog2 ...])
    %   t = dialogmgr.DialogTest(Dialog1, Dialog2, ...)

%   Copyright 2014-2020 The MathWorks, Inc.
    
    properties (SetAccess=private)
        DP           % handle to DialogPresenter
        Dialogs      % vector of handles to all Dialogs
        Listeners
    end
    
    properties % (Access=private)
        hFig         % handle to HG figure
        hTop         % handle to top-level uicontainer
        hAppLine     % handle to line showing noisy sinewave
        hAppCursor   % handle to cursor point
    end
    
    methods
        function dt = DialogTester(varargin)
            % Pass one or more dialogs to the constructor:
            %   dt = DialogTester(dlg1,dlg2,...)
            %   dt = DialogTester([dlg1 dlg2 ...])
            %
            % Set property values for the DialogPresenter by passing them
            % as trailing arguments to the constructor:
            %    dt = DialogTester(...,'Param',value,...)
            %
            % To identify DialogPresenter properties, look at:
            %    dt.DP
            
            pv = parseArgs(dt,varargin);
            init(dt,pv);
        end
    end
    
    methods (Access=private)
        function init(dt,pv)
            % Initialize overall UI
            % - create dialog panel
            % - create app body with simple plot
            
            % Create UI
            fig = figure( ...
                'NumberTitle','off', ...
                'Name',getAppName(dt), ...
                'IntegerHandle','off', ...
                'menubar','none', ...
                'pos', [950 500 850 450]);
            
            dt.hFig = fig;
            hHigher = uicontainer( ...
                'parent',fig, ...
                'tag','DialogTester_hTop_Parent', ...
                'pos',[0 0 1 1]);
            dt.hTop = uicontainer( ...
                'parent',hHigher, ...
                'tag','DialogTester_hTop', ...
                'pos',[0 0 1 1]);
            
            createDP(dt,pv);
            addDialogs(dt);
            
            createAppBody(dt);
            finalizeDialogs(dt); % makes DialogPanel visible when finished
            updateAppBody(dt);
            initListeners(dt);
            
            % Enable UI
            setVisible(dt.DP,true);
        end
        
        function name = getAppName(dt)
            % Name to use in figure title bar
            
            dlgs = dt.Dialogs;
            Nd = numel(dlgs);
            if Nd==1
                s = '(1 dialog)';
            else
                s = sprintf('(%d dialogs)',Nd);
            end
            name = ['Dialog Tester ' s];
        end
        
        function pv = parseArgs(dt,args)
            % Parse input args to constructor.
            % - args is a cell vector of input args.
            % - Sets .Dialogs to vector of dialog handles.
            % - Returns remaining P/V pairs that are applied to
            %   DPVerticalPanel
            %
            % Could be a vector of dialogs
            % - 1 arg: scalar or vector of dialog objects
            % - N args: multiple scalar dialog objects
            
            if nargin<1
                error(message('Spcuilib:dialogmgr:ErrorMustSpecifyDialogHandle'));
            end
            
            % Create baseline ("default") param/value pairs
            % pv = {};
            pv = {'DialogBorderDecoration', ...
                {'TitlePanelBackgroundColorSource','Custom', ...
                'TitlePanelBackgroundColor',[.1 .1 .8]}, ...
                'DialogBorderFactory', @dialogmgr.DBTopBar, ...
                'DialogVerticalGutter',8, ...
                'DialogHorizontalGutter',8, ...
                'Animation',true};
            
            % Identify if P/V pairs are passed
            % - Dialog handles are passed to constructor first.
            % - Remaining args are P/V pairs.
            %
            % We find the FIRST char arg and assume it's the START of
            % property/value pairs
            idx = find(cellfun(@ischar,args),1);
            if ~isempty(idx)
                pv = [pv args(idx:end)]; % trailing P/V args
                args = args(1:idx-1);    % initial dialog args
            end
            
            if numel(args) > 1
                % More than one dialog handle was passed
                % - verify that multiple SCALAR dialog handles are passed
                if any(cellfun(@numel,args)~=1)
                    error(message('Spcuilib:dialogmgr:ErrorMultipleScalarDialogsExpected'));
                end
                dlgs = [args{:}].'; % force into a row
            else
                % Single arg is a vector of dialog handles
                dlgs = args{1};
                dlgs = dlgs(:).'; % force into a row
            end
            dt.Dialogs = dlgs;
        end
        
        function initListeners(dt)
            % Create listeners in disabled state.
            
            % Suppress keypresses (ordinarily, keypresses over the app
            % would echo to the command window)
            hfig = dt.hFig;
            hfig.KeyPressFcn = @(~,~)[];
            
            lis.MouseMotion  = addlistener(hfig,'WindowMouseMotion', @(h,e)mouseMove(dt,h,e));
            lis.MousePress   = addlistener(hfig,'WindowMousePress',  @(~,~)mouseDown(dt));
            lis.MouseRelease = addlistener(hfig,'WindowMouseRelease',@(~,~)mouseUp(dt));
            lis.ScrollWheel  = addlistener(hfig,'WindowScrollWheel', @(~,e)wheelMove(dt,e));
            dt.Listeners = lis;
        end
        
        function createAppBody(dt)
            % This is the test app
            
            theDP = dt.DP;        % Get handle to DialogPanel
            dt.hFig = theDP.hFig; % copy for convenience
            
            % Create axes within DialogPanel body panel
            ax = axes( ...
                'Parent',theDP.hBodyPanel, ...
                'Position',[.08 .1 .9 .86], ...
                'Color', 'w',...
                'TickDir','out', ...
                'Units','norm', ...
                'Tag','DialogTester_hAppBodyAxes', ...
                'UIContextMenu',theDP.hContextMenu, ...
                'XTickLabel','', ...
                'XLim',[0 1], ...
                'YLim',[-1.2 1.2]);
            
            % Add noisy sine-wave data
            x = (0:1000).'/1000;
            y = zeros(size(x));
            dt.hAppLine = line( ...
                'Parent',ax, ...
                'Color','b', ...
                'XData',x, ...
                'YData',y);
            
            % Add cursor to show mouse position / motion
            dt.hAppCursor = line( ...
                'Parent',ax, ...
                'Color','r', ...
                'XData',[0 0], ...
                'YData',[-1 1], ...
                'LineStyle','-', ...
                'Marker','o');
        end
        
        function updateAppBody(dt)
            % Update values of noisy sinewave.
            
            x = dt.hAppLine.XData;
            y = sin(4*pi*x - pi/2 + (rand(size(x))-0.5)/2 );
            set(dt.hAppLine,'YData',y);
        end
        
        function createDP(dt,pv)
            % Create DPVerticalPanel object that is owned by tester.
            % Panel is left invisible until after dialogs are added.
            
            % Suppress automatic call to init() by not passing hTop as
            % input arg. Just call init() manually after properties are
            % set.
            theDP = dialogmgr.DPVerticalPanel;
            
            % Ordered list of names of visible dialogs
            theDP.DockedDialogNamesInit = {dt.Dialogs.Name};
            
            % Set property values on DP:
            % NOTE: "set" is not supported by DialogPresenter base class
            %if ~isempty(pv)
            %    set(theDP,pv{:});
            %end
            Npv = numel(pv)/2;
            if Npv~=floor(Npv)
                error(message('Spcuilib:dialogmgr:ErrorMustHavePVPairs'));
            end
            for i = 1:Npv
                theDP.(pv{2*i-1}) = pv{2*i};
            end
            
            % Cross-reference DialogTester and DialogPresenter
            dt.DP = theDP;
            theDP.UserData = dt;
            
            init(theDP,dt.hTop);
        end
        
        function addDialogs(dt)
            % Create application dialogs and add to Dialog Manager.
            
            dlgs = dt.Dialogs;
            N = numel(dlgs);
            for i = 1:N
                d_i = dlgs(i);
                createAndRegisterDialog(dt.DP,d_i);
                
                % For each dialog, any change to a control can call a
                % function.  It can be a different function for each
                % dialog.  For simplicity, we call the SAME function for
                % ALL dialogs in this test app:
                onPropertyChange(d_i, @(h,ev)valuesUpdated(dt,ev));
            end
        end
        
        function finalizeDialogs(dt)
            % Leaves GUI ready for user interaction, but invisible
            
            theDP = dt.DP;
            
            % First step of init is to finalize dialogs for dialog panel
            % -> no more dialogs should be registered after this point!
            finalizeDialogRegistration(theDP);
            
            % true: show panel
            % false: no animation
            setDialogPanelVisible(theDP,true,false);
            
            % Initialize pointer
            setptr(theDP.hFig,'arrow');
            
            % Add a resize callback if desired:
            % set(theDP.hBodyPanel,'ResizeFcn',@resizeFcn);
        end
    end
    
    % Mouse interaction
    %
    methods (Access=private)
        function wheelMove(dt,ev)
            % Mouse scroll wheel modified/moved
            
            % Allow DialogPanel to handle wheel events in its area
            handled = mouseScrollWheel(dt.DP,ev);
            if ~handled
                % Do something useful here!
            end
        end
        
        function mouseDown(dt)
            % Mouse button pressed
            
            % Let DialogPanel handle any mouseDown events of its own first
            motionFcn = mouseDown(dt.DP);
            if ~isempty(motionFcn)
                % Allow DialogManager to install its own motion:
                dt.Listeners.MouseMotion.Callback = motionFcn;
            else
                % For this app, when clicking, replace the vertical-bar
                % cursor with a single dot on the data value.
                %
                % Shut down motion while button is down:
                dt.Listeners.MouseMotion.Enabled = false;
                
                % Find the y-value based on current mouse position:
                % selType = get(dt.hFig,'SelectionType');
                all_x = dt.hAppLine.XData;
                all_y = dt.hAppLine.YData;
                pt = get(gca,'CurrentPoint');
                this_x = pt(1,1);
                idx = find(this_x <= all_x,1,'first');
                if isempty(idx)
                    idx = 1;
                end
                this_y = all_y(idx);
                set(dt.hAppCursor, ...
                    'LineWidth',4, ...
                    'XData',this_x, ...
                    'YData',this_y);
            end
        end
        
        function mouseUp(dt)
            % Invoked when the mouse button has been released
            
            % DialogPanel handles mouse up event first.
            % If it doesn't need it, it passes control to the app:
            handled = mouseUp(dt.DP);
            if handled
                % DialogManager took the mouse listeners previously, so we
                % re-install ours now:
                dt.Listeners.MouseMotion.Callback = @(h,e)mouseMove(dt,h,e);
            else
                % Mouse button up in app: restore vertical-bar cursor
                pt = get(gca,'CurrentPoint');
                x = [pt(1,1) pt(1,1)];
                y = [-1 1];
                set(dt.hAppCursor, ...
                    'LineWidth',0.5, ...
                    'XData',x, ...
                    'YData',y);
            end
            
            % Our app disables motion in mouseDown, so we must
            % re-enable motion in mouseUp
            dt.Listeners.MouseMotion.Enabled = true;
        end
        
        function mouseMove(dt,hFig,ev)
            % Give feedback to user while mouse is in motion.
            
            if nargin > 2
                hFig.CurrentPoint = ev.Point;
            end
            
            % Let DialogPanel handle mouse move events first
            handled = mouseMove(dt.DP);
            if ~handled
                % React to mouseMove in app
                pt = get(gca,'CurrentPoint');
                x = [pt(1,1) pt(1,1)];
                y = [-1 1];
                set(dt.hAppCursor, ...
                    'LineWidth',0.5, ...
                    'XData',x, ...
                    'YData',y);
            end
        end
    end
    
    methods (Access=private)
        function valuesUpdated(dt,ev)
            % Do whatever the App wants to do in response to a change in
            % dialog parameters.
            
            % Here's how to get the specific property that changed, and its
            % new value:
            theDialog = ev.Source;
            theProperty = ev.Property;
            newValue = theDialog.(theProperty);
            
            % We just show the change in the command window:
            valueStr = mat2str(newValue);
            fprintf('Property "%s" changed to %s\n',theProperty,valueStr);
            
            % Update the signal noise and redisplay:
            updateAppBody(dt);
        end
    end
end
