classdef PromptForPasswordDialog < handle
    % Open a dialog box for the user to enter a password. Returns a token to
    % retrieve the password from the PasswordFieldStorage.

    % Copyright 2021-2023 The MathWorks, Inc.

    properties (Constant, Hidden)
        % Due to bugs in uifigure resize we try and estimate the final size
        % to minimize jitter
        EstimatedWidth = 280
        EstimatedHeight = 105
    end

    properties (Access = private)
        PasswordField
        PasswordToken

        UIFigure
        GridLayout

        GetOutputFcn = @iNoop

        OKButton
        CancelButton
    end

    properties (Hidden)
        % Allow tests to prevent exit on Enter to work around g2818314
        ExitOnOK = true
    end

    methods (Hidden)
        function fig = hGetFigure(obj)
            % Access to figure for testing
            fig = obj.UIFigure;
        end

        function varargout = hGet(obj)
            % Access to get output for testing
            [varargout{1:nargout}] = obj.GetOutputFcn();
        end
    end

    methods
        function varargout = get(obj)
            % Blocks until figure is closed then returns the result of GetOutputFcn
            waitfor(obj.UIFigure);
            [varargout{1:nargout}] = obj.GetOutputFcn();
        end
    end

    methods (Static)
        function token = prompt(title, msg)
            dialog = matlabshared.internal.PromptForPasswordDialog(title, msg);
            token = dialog.get();
        end
    end

    methods
        function obj = PromptForPasswordDialog(title, msg)
            obj.UIFigure = uifigure( ...
                Name=title, ...
                WindowStyle='modal', ...
                Resize='on', ...
                AutoResizeChildren='off', ...
                Visible='off', ...
                HandleVisibility='off');
            matlab.graphics.internal.themes.figureUseDesktopTheme(obj.UIFigure);
            obj.UIFigure.CloseRequestFcn = @(~, ~) obj.closeDelete();
            obj.UIFigure.KeyPressFcn = @(~, data) obj.handleKeyPress(data);

            obj.GridLayout = uigridlayout(obj.UIFigure);
            obj.GridLayout.Padding = [10 10 10 10];
            obj.GridLayout.RowSpacing = 10;
            obj.GridLayout.ColumnSpacing = 10;

            obj.UIFigure.Position(3) = obj.EstimatedWidth;
            obj.UIFigure.Position(4) = obj.EstimatedHeight;

            obj.GridLayout.RowHeight = {'fit', 24, 24};
            obj.GridLayout.ColumnWidth = {140, 40, 60};

            % Add dialog elements
            obj.addHelpText(1, [1 3], msg);
            obj.addPasswordField(2, [1 3]);
            obj.addOKButton(true, 3, 2);
            obj.addCancelButton(3, 3);

            obj.show();
        end
    end

    methods (Access = private)
        function element = addElement(obj, createElementFcn, row, col)
            % Add a ui element to the grid at the given row and column
            element = createElementFcn(obj.GridLayout);
            element.Layout.Row = row;
            element.Layout.Column = col;
        end

        function show(obj)
            % Show the complete UI and shrink the figure height to match
            % the contents
            movegui(obj.UIFigure, 'center');
            matlab.ui.internal.PositionUtils.fitToContent(obj.UIFigure);
            obj.UIFigure.Visible = 'on';
        end

        function addHelpText(obj, rows, cols, msg)
            helpText = obj.addElement(@uilabel, rows, cols);
            helpText.Tag = 'helpText';
            helpText.WordWrap = 'on';
            helpText.Text = msg;
            helpText.HorizontalAlignment = 'Left';
        end

        function addPasswordField(obj, rows, cols)
            obj.PasswordField = obj.addElement(@(g) matlab.ui.control.internal.PasswordField('Parent', g), rows, cols);
            obj.PasswordToken = '';
            obj.PasswordField.PasswordEnteredFcn = @obj.updatePasswordToken;
            obj.PasswordField.EnablePlainTextControl = matlab.lang.OnOffSwitchState('on');
            obj.PasswordField.Tag = 'PasswordField';
        end

        function addOKButton(obj, enable, row, col)
            obj.OKButton = obj.addElement(@uibutton, row, col);
            obj.OKButton.Text = getString(message('shared_readsecret:internal:OK'));
            obj.OKButton.ButtonPushedFcn = @(~, ~) obj.okDelete();
            obj.OKButton.Enable = enable;
            obj.OKButton.Tag = 'OKButton';
        end

        function addCancelButton(obj, row, col)
            obj.CancelButton = obj.addElement(@uibutton, row, col);
            obj.CancelButton.Text = getString(message('shared_readsecret:internal:Cancel'));
            obj.CancelButton.ButtonPushedFcn = @(~, ~) obj.closeDelete();
            obj.CancelButton.Tag = 'cancelButton';
        end

        function handleKeyPress(obj, data)
            if strcmp(data.Key, 'return') && obj.OKButton.Enable && obj.ExitOnOK
                obj.okDelete();
            elseif strcmp(data.Key, 'escape') && obj.CancelButton.Enable
                obj.closeDelete();
            end
        end

        function updatePasswordToken(obj, ~, evt)
            % Remove the previous (unused password) from the store
            iSafeDeletePasswordToken(obj.PasswordToken);
            % Remember the new token
            obj.PasswordToken = evt.Token;
            % Handle the pressed key in the same way as the parent uifigure
            obj.handleKeyPress(struct('Key', obj.UIFigure.CurrentKey));
        end

        function okDelete(obj)
            token = obj.PasswordToken;
            obj.GetOutputFcn = @() token;
            delete(obj.UIFigure);
        end

        function closeDelete(obj)
            obj.GetOutputFcn = @iError;
            delete(obj.UIFigure);
        end
    end
end

function varargout = iError() %#ok<STOUT>
error(message('shared_readsecret:internal:NoPasswordEntered'));
end

function iSafeDeletePasswordToken(token)
% Remove password from storage for corresponding token if it exists. Do not
% error if it does not.
if ~isempty(token)
    try
        matlabshared.internal.clearStoredPassword(token);
    catch
    end
end
end

function iNoop()
end
