classdef polariMBMagTicks < internal.polariMouseBehavior
    
    methods
        function obj = polariMBMagTicks(isButtonDown)
            
            if nargin > 0 && isButtonDown
                obj.InstallFcn  = @showToolTipAndPtr_buttondown;
                obj.MotionEvent = @wbdrag;
                obj.DownEvent   = [];
                obj.UpEvent     = @wbup;
                obj.ScrollEvent = [];
            else
                obj.InstallFcn  = @showToolTipAndPtr;
                obj.MotionEvent = @internal.polariMBMagTicks.wbmotion;
                obj.DownEvent   = @wbdown;
                obj.UpEvent     = [];
                obj.ScrollEvent = @internal.polariMBMagTicks.wbscroll;
            end
        end
    end
    
    % Mouse: MagTicks
    %
    %   Normal: - change upper or lower bound of MagnitudeLim
    %             (and switch to manual MagnitudeLimMode)
    %           - change angle of magnitude line
    %             (and switch to manual MagnitudeAxisAngleMode)
    %
    %   Shift:  - simultaneous change to upper and lower mag limits
    %
    methods (Static)
        function wbscroll(p,ev,action)
            % Scale upper (max) or lower (min) mag limit
            %
            % Typical use is invocation via scroll-wheel.
            
            if nargin < 3
                action = 'max';
            end
            if ~strcmpi(p.MagnitudeLimMode,'manual')
                enableListeners(p,false);
                p.MagnitudeLimMode = 'manual';
                enableListeners(p,true);
            end
            
            % Max VerticalScrollCount is +/-16.
            % We scale that to +/-1.
            wheelScale = ev.VerticalScrollCount / 16;
            
            newLim = p.pMagnitudeLim; % unscaled, [min max]
            del = (newLim(2) - newLim(1)) * wheelScale/2;
            if strcmpi(action,'max')
                del = [0 del];
            else
                del = [del 0];
            end
            newLim = newLim + del;
            
            % Glow could be enabled when scroll-wheel engages.  Glow never
            % occurs for mouse-down drags "off" the dataset line; it can
            % only happen when mouse hovers over dataset line THEN scroll
            % wheel used.  So rather than execute this code for every
            % mag-zoom operatoin (eg, rather than put it in
            % updateMagnitudeLim), we put it here so only scroll-tick
            % suffers the penalty.
            plot_glow(p,false);

            updateMagnitudeLim(p,newLim,'default');
            
            % xxx needed?
            updateMarkers(p); % if angle marker datadots are enabled
        end
        
        function wbmotion(p,ev)
            
            firstEntry = p.ChangedState;
            shiftChanged = firstEntry || ...
                p.pLastShiftKeyPressed ~= p.pShiftKeyPressed;
            
            s = computeHoverLocation(p,ev);
            if firstEntry
                p.ChangedState = false;
                
                % Less noisy display when mag-ticks don't hilite on
                % mouse-over, just on mouse-down:
                if p.MagHiliteOnHover
                    hiliteMagAxisDrag_Init(p,'on');
                end
            end
            
            if s.overMarker
                % higher priority than, and overlaps with, mag tick axis
                
                % Not over mag ticks
                hiliteMagAxisDrag_Init(p,'off');
                
                % Return mag tick vis to default state, which may be
                % invisible:
                overrideMagnitudeTickLabelVis(p,'default');
                
                autoChangeMouseBehavior(p,s);
                
            elseif s.overMagnitudeTicks
                % Responds dynamically to changes in shift-key presses,
                % regardless of whether the mouse moved.  Mouse motion
                % callback is invoked manually by FigKeyPress().
                
                if firstEntry && strcmpi(p.MagnitudeTickLabelColorMode,'auto')
                    % Temporary change in tick color while hovering
                    
                    c = internal.ColorConversion.getBWContrast(p.GridBackgroundColor);
                    p.pMagnitudeTickLabelColor = c;
                    set(p.hMagText,'Color',c);
                end
                
                if shiftChanged && p.pShiftKeyPressed
                    % Shift means translate, not re-scale.
                    %
                    % Shut off lower/upper highlighting to signify
                    % translate instead of scaling one of the limits:
                    %hiliteMagAxisDrag_Init(p,'off');
                    
                    if p.MagHiliteOnHover
                        % Display both up/dn hilite arrows for translate
                        hiliteMagAxisDrag_Init(p,'update','upperlower');
                    end
                end
                
                if ~p.pShiftKeyPressed
                    % Re-scale upper or lower limit, if mouse gets pressed.
                    %
                    % o and s.radius both have units of normalized radius
                    o = p.MagDrag_OrigRadius;
                    p.MagDrag_OrigRadius = s.radius;
                    brkpt = getMagTickHoverBrkpt(p);
                    if firstEntry || ...
                            (s.radius > brkpt && o <= brkpt) || ...
                            (s.radius <= brkpt && o > brkpt)
                        % Mouse hover moved from upper- to lower-half or
                        % vice-versa:
                        showToolTipAndPtr(p,s.radius,brkpt);
                        
                        % call is internally protected from continuous
                        % calls during motion:
                        if p.MagHiliteOnHover
                            hiliteMagAxisDrag_Init(p,'update');
                        end
                        %changeMouseBehavior('magticks');
                        
                    elseif shiftChanged
                        % We would make this call every time shiftChanged
                        % in this section, but since the above code also
                        % does this, we try not to do it twice in
                        % succession:
                        if p.MagHiliteOnHover
                            hiliteMagAxisDrag_Init(p,'update');
                        end
                    end
                end
            else
                % Not over mag ticks (possibly not over axes)
                
                hiliteMagAxisDrag_Init(p,'off');
                
                % Return mag tick vis to default state, which may be
                % invisible:
                overrideMagnitudeTickLabelVis(p,'default');
                
                if strcmpi(p.MagnitudeTickLabelColorMode,'auto')
                    % Restore tick color when hovering done
                    updateMagnitudeTickLabelColor(p);
                    set(p.hMagText,'Color',p.pMagnitudeTickLabelColor);
                end
                autoChangeMouseBehavior(p,s);
                
                % This is done for a fix: on small motion to bring pointer
                % outside the mag ticks, the pointer changes, the motion
                % gets remapped (to nothing), but the tooltip remains -
                % incorrect guidance now.
                %resetToolTip(p);
            end
            
            % Update backing store for key press state:
            if  p.pShiftKeyPressed ~= p.pLastShiftKeyPressed
                p.pLastShiftKeyPressed = p.pShiftKeyPressed;
            end
        end
        
        function openPropertyEditor(p)
            % Open dialog enabling user to change:
            %  - MagnitudeLim = [min max]
            %  - MagnitudeLimMode = 'auto','manual'
            %  - MagnitudeTick = vector of tick locations
            %  - MagnitudeTickMode = 'auto','manual'
            
            % Get current values as "pretty" strings
            %
            p1 = internal.polariCommon.sprintfNumTotalDigitsAsVector(p.MagnitudeLim,4);
            p2 = internal.polariCommon.sprintfNumTotalDigitsAsVector(p.MagnitudeTick,4);
            p3 = sprintf('%g',p.MagnitudeFontSizeMultiplier);
            
            % Open dialog
            %
            prompt = { ...
                'Magnitude Limits:', ...
                'Magnitude Ticks:', ...
                'Font Size Multiplier:'};
            name = 'Magnitude Axis';
            numlines = 1;
            defaults = {p1,p2,p3};
            options.Resize='on';
            options.WindowStyle='modal';
            options.Interpreter='tex';
            a = inputdlg(prompt,name,numlines,defaults,options);
            
            % Process values
            %
            % Empty returned if cancel pressed
            if ~isempty(a)
                % Silently ignore bad values
                try
                    % Don't apply value if it is the same.  Setting the
                    % value will force 'manual' mode, which may be 'auto'
                    % at present.  With no change to value, we keep 'auto'.
                    %
                    % HOWEVER ... our digit-truncation comes in to play
                    % here, and will show up as a change if we compare to
                    % the original value.  So we use the "original
                    % truncated string" and the new user string.
                    %
                    % Overall, this whole test is a judgment call.  We
                    % really need checkboxes in the dialog to convey user
                    % intention concretely.
                    
                    if ~isequal(a{1},p1)
                        p.MagnitudeLim = str2num(a{1}); %#ok<*ST2NM>
                    end
                    if ~isequal(a{2},p2)
                        p.MagnitudeTick = str2num(a{2});
                    end
                    if ~isequal(a{3},p3)
                        p.MagnitudeFontSizeMultiplier = str2num(a{3});
                    end
                catch me
                    warndlg(me.message,'Invalid Input','modal');
                end
            end
        end
    end
end

function wbdown(p,ev)
% Click on magnitude axis allows:
%   - mag tick angle change, or
%   - mag tick upper/lower bound change (scaling).
%
% Double-click forces mag-limit mode to 'auto'.

%resetToolTip(p);

p.hFigure.CurrentPoint = ev.Point;

% Get radius mouse coords before other work
pt = get(p.hAxes,'CurrentPoint');

st = p.hFigure.SelectionType;
if strcmpi(st,'open')
    % double-click on magnitude axis tick region
    internal.polariMBMagTicks.openPropertyEditor(p);
    return
elseif strcmpi(st,'alt')
    % right-click: this action launches the context menu.
    %
    % Prevent further reaction to right-click here, so we don't
    % do mag-tick hilite and confuse the user when the context
    % menu launches.
    resetToolTip(p);
    return
end

% Get radius mouse coords, setup cache entries
% Do before enabling new mouse behaviors
pt = pt(1,1:2);
r = norm(pt); %r = hypot(pt(2),pt(1));
p.AngleDrag_StartAngleClicked = atan2d(pt(2),pt(1));
p.AngleDrag_StartShiftPressed = p.pShiftKeyPressed;
p.MagDrag_OrigMagnitudeLim    = p.pMagnitudeLim;
p.MagDrag_ChangedMagnitudeLim = false;

% To do "mag line endpoint pull", we can't use the unit-circle
% origin as reference.  We use the point clicked as origin, and
% two points equidistant from it that are along the mag-angle
% line: one far away along the -ve direction, and one far away
% along the +ve direction.
%
% We then compare the current (x,y) point to these two points,
% and depending on which one we're closer to, we incr or decr
% the lim(1) or lim(2) point.  Note that lim(1) vs lim(2)
% depends solely on where we first clicked.
%
% Capture the points:

% need "normalized angle" of mag angle line
lineAng = p.pMagnitudeAxisAngle;
if strcmpi(p.AngleDirection,'ccw')
    lineAngNorm = lineAng - (p.AngleAtTop-90);
else
    lineAngNorm = (p.AngleAtTop+90) - lineAng;
end
lineAngNorm = lineAngNorm*pi/180;

hoverBrkpt = getMagTickHoverBrkpt(p);
s.InitialPointClicked = pt;
s.LineUnitVecCol = [cos(lineAngNorm); sin(lineAngNorm)];
s.ChangingLowerLim = r <= hoverBrkpt;
p.MagDrag_MouseDown = s;

p.MagDrag_ChangedAngle = false;
p.MagDrag_AngleDelta = 0; % Initialize before drag

% Set the interactive angle-rotation quantization angle to be
% half the angle between angle labels, or 15 degrees, whichever
% is smaller.
if p.pShiftKeyPressed
    % Smooth rotation of axis angle, or
    % translate versus scaling of mag axis
    dth = 0;
else
    % Quantized rotation normally
    S = p.pAngleLabelCoords;
    dth = min(15, abs(S.th(2)-S.th(1))*90/pi);
end
p.pAngleRotationStepSize = dth;

hideAngleMarkerDataDots(p,true);
if ~p.MagHiliteOnHover
    hiliteMagAxisDrag_Init(p,'on');
end

% Determine which side the lim-change would go, based on
% whether the cursor is over the first half or second half of
% the mag tick line
changeMouseBehavior(p,'magticks_buttondown');
end

function wbdrag(p,ev)
% Get mouse pointer in local axis reference frame.
%
% For mag-line angle rotation:
%   normal drag: quantized angle rotations
%   shift drag: continuous angle rotations
%
%   Compute delta-theta such that ADDING it to MagnitudeAxisAngle
%   would yield new angle for mag line.
%
% For magnitude-lim change:
%    normal drag: increase/decrease lower/upper limits
%
%    shift drag: NO OPERATION (too clumsy when continuous angle
%    changes are being performed)

p.hFigure.CurrentPoint = ev.Point;

pt = p.hAxes.CurrentPoint(1,1:2);

% cached once at time of button-down
%shiftPressed = p.AngleDrag_StartShiftPressed;
shiftPressed = p.pShiftKeyPressed; % live updates
shiftChanged = shiftPressed ~= p.AngleDrag_StartShiftPressed;
if shiftChanged
    % Shift-state changed
    
    % First, record this as the new shift state:
    p.AngleDrag_StartShiftPressed = shiftPressed;
    
    % Now, need to register the current drag with new
    % shift-state, as if mouse-up occurred, then re-engage with
    % mouse-down:
    %
    % Cache and restore mag/change change state, to keep
    % constraints:
    c1 = p.MagDrag_ChangedMagnitudeLim;
    c2 = p.MagDrag_ChangedAngle;
    
    wbup(p,ev);
    wbdown(p,ev);
    
    p.MagDrag_ChangedMagnitudeLim = c1;
    p.MagDrag_ChangedAngle = c2;
    
    wbdrag(p,ev); % kick it to reset mouse cursors
    return
end

% Perform magnitude lim change, if criteria met
%
s = p.MagDrag_MouseDown;
if ~p.MagDrag_ChangedAngle
    % Check for magnitude lim change
    
    % Find distance of mouse-drag, from initial click, based on
    % direction of mag-angle line.
    %
    % Do a vector dot-product to project point onto mag-line,
    % giving normalized (unit-radius) distance traveled along
    % mag-line:
    ptLocalRow = pt - s.InitialPointClicked;
    normdist = ptLocalRow * s.LineUnitVecCol;  % inner (dot) product
    
    % Detect radial-direction motion
    %  - give a little dead-band to decide radial-vs-angle
    %  - require amount of motion cross a threshold, otherwise
    %     a possibel angle-change may be ignored
    %
    radialMotionDetect = p.MagDrag_ChangedMagnitudeLim;
    if ~radialMotionDetect
        % Scale the threshold by the distance
        %  - Close to outer edge, use a HIGH threshold, so user
        %  can more easily convey an angle change (it takes
        %  lots of pixels of distance to convey angle change at
        %  a high radius, too easy to invoke a radial change)
        %
        %  - Close to origin, use a LOW threshold, since
        %  angle-change is too easy to detect (lots of angular
        %  change just pixels from current mouse pos)
        raw_dist = hypot(pt(1),pt(2));
        th = p.MagLimChangeDetect_MinNormDist;
        adapt_th = th(1) * (1-raw_dist) + th(2) * raw_dist;
        radialMotionDetect = abs(normdist) > adapt_th;
    end
    
    if radialMotionDetect
        % Scale by mag radius limits for true scaled distance
        % traveled (i.e., no longer normalized radius):
        origLim = p.MagDrag_OrigMagnitudeLim; % unscaled
        origLimDiff = origLim(2) - origLim(1);
        limChange = normdist * origLimDiff;
        
        % Apply change to upper or lower limit.
        %
        % Limit that is changing will be kept 1 percent distant
        % from other endpoint (it can move 99 percent of the
        % way toward other limit).
        newLim = origLim;
        
        if shiftPressed
            % Translate magnitude range
            % (change both upper and lower mag tick)
            newLim = newLim - limChange;
            
            hiliteMagAxisDrag_Init(p,'update','upperlower');
        else
            % Scale: Modify upper or lower mag tick range to
            % achieve range scaling.
            
            if s.ChangingLowerLim
                action = 'lower';
            else
                action = 'upper';
            end
            hiliteMagAxisDrag_Init(p,'update',action);
            
            guard = 0.01 * origLimDiff;
            if s.ChangingLowerLim
                newLim(1) = newLim(1) - limChange;
                if newLim(1) >= newLim(2) - guard
                    newLim(1) = newLim(2) - guard;
                end
            else
                newLim(2) = newLim(2) - limChange;
                if newLim(2) <= newLim(1) + guard
                    newLim(2) = newLim(1) + guard;
                end
            end
        end
        
        if ~isequal(newLim, p.pMagnitudeLim)
            % Operations to do once per down-drag-up sequence
            if ~p.MagDrag_ChangedMagnitudeLim
                p.MagDrag_ChangedMagnitudeLim = true;
                
                enableListeners(p,false);
                p.MagnitudeLimMode = 'manual'; % AbortSet makes this efficient
                enableListeners(p,true);
                
                resetToolTip(p);
            end
            updateMagnitudeLim(p,newLim);
            
            return % EARLY EXIT
        end
    end
end

if p.MagDrag_ChangedMagnitudeLim
    % Once we've begun changing magnitude limits, disallow
    % any angle changes until mouse-up
    return % EARLY EXIT
end

% Disallow angle rotation from LOWER mag limit.
% Too sensitive with respect to angle to allow this.
% Only allow from UPPER mag limit.
if s.ChangingLowerLim
    return % EARLY EXIT
end

% Compute theta rotation
%
th = atan2(pt(2),pt(1)) * 180/pi;  % normalized polar angle (0=east always)
del_th = th - p.AngleDrag_StartAngleClicked; % subtract normalized angles
if strcmpi(p.AngleDirection,'ccw')
    del_th = -del_th;
end

% See if change in shift-state occurred
if shiftPressed ~= p.pShiftKeyPressed
    % Change angle step-size
    if p.pShiftKeyPressed
        % Smooth rotation when shift pressed
        dth = 0;
    else
        % Quantized rotation normally
        S = p.pAngleLabelCoords;
        dth = min(15, abs(S.th(2)-S.th(1))*90/pi);
    end
    p.pAngleRotationStepSize = dth;
    
    % Update shift-state for change detection:
    p.AngleDrag_StartShiftPressed = p.pShiftKeyPressed;
end

% Impose quantization on angle step-size
stepSize = p.pAngleRotationStepSize;
if stepSize > 0
    % Quantize angle step
    del_th = round(del_th/stepSize)*stepSize;
    
    % Add offset to del_th that puts mag angle back on a
    % quantized angle position:
    t  = p.pMagnitudeAxisAngle;
    tq = round(t/stepSize)*stepSize;
    del_th = del_th + t-tq;
else
    % Clear out if very close to zero
    if abs(del_th) < 1e-3
        del_th = 0;
    end
end

% Only mark as changed if non-zero, and past a small threshold
% to prevent work for small "chatter"
%
angularMotionDetect = (del_th ~= p.MagDrag_AngleDelta);
if angularMotionDetect % must be true to continue
    angularMotionDetect = p.MagDrag_ChangedAngle;
    if ~angularMotionDetect
        raw_dist = hypot(pt(1),pt(2));
        th = p.MagAngleChangeDetect_MinTheta;
        adapt_th = th(1) * (1-raw_dist) + th(2) * raw_dist;
        angularMotionDetect = abs(del_th) > adapt_th;
    end
end
if angularMotionDetect
    p.MagDrag_AngleDelta = del_th;
    p.MagDrag_ChangedAngle = true;
    
    if p.ChangedState
        p.ChangedState = false;
        resetToolTip(p);
    end
    
    % Wrap angle into proper range for AngleRange
    th = p.pMagnitudeAxisAngle - p.MagDrag_AngleDelta;
    if strcmpi(p.AngleRange,'180')
        % Put th in range (-180,+180]
        th = th - floor((th+180)/360)*360;
        if th==-180
            th = 180;
        end
    else
        % Put th in range [0,359]
        th = th - floor(th/360)*360;
    end
    
    i_updateAngleOfLabelMagnitudes(p,th);
end
end

function wbup(p,ev)

% Set point into axes:
p.hFigure.CurrentPoint = ev.Point;

hiliteMagAxisDrag_Init(p,'off');

% Restore tick color for 'auto' mode
if strcmpi(p.MagnitudeTickLabelColorMode,'auto')
    updateMagnitudeTickLabelColor(p);
    set(p.hMagText,'Color',p.pMagnitudeTickLabelColor);
end

del = p.MagDrag_AngleDelta;
if del ~= 0
    % Key task: change public (user's) property value
    %
    % Wrap angle into proper range for AngleRange
    th = p.pMagnitudeAxisAngle - p.MagDrag_AngleDelta;
    if strcmpi(p.AngleRange,'180')
        % Put th in range (-180,+180]
        th = th - floor((th+180)/360)*360;
        if th==-180
            th = 180;
        end
    else
        % Put th in range [0,359]
        th = th - floor(th/360)*360;
    end
    
    % Clear out states used during drag (not essential)
    p.AngleDrag_StartAngleClicked     = []; % normalized unit circle
    p.MagDrag_AngleDelta              = 0;  % *Must* reset to 0
    
    % Update primary property, throws event to update plot
    p.MagnitudeAxisAngle = th;
    
end
% Reset these flags every time, as they suppress display of
% the up/down or left/right overlay arrows if they "linger" in
% a true-state.
p.MagDrag_ChangedMagnitudeLim = false;
p.MagDrag_ChangedAngle = false;

hideAngleMarkerDataDots(p,false);

s = computeHoverLocation(p,ev);
if s.overMagnitudeTicks
    changeMouseBehavior(p,'magticks');
    internal.polariMBMagTicks.wbmotion(p,ev);
else
    % Update mouse handlers
    autoChangeMouseBehavior(p,s);
end
end

function showToolTipAndPtr(p,r,brkpt)
% Update mouse pointer and tooltip when hovering over mag tick
% axis
%
setptr(p.hFigure,'hand');

if nargin < 3
    brkpt = getMagTickHoverBrkpt(p);
end
if nargin < 2
    pt = p.hAxes.CurrentPoint;
    r = norm(pt(1,1:2));
end
lowerLim = r < brkpt;
showToolTip(p,lowerLim);
end

function showToolTip(p,changingLowerLim)
if p.ToolTips && ~isempty(p.hToolTip) && isvalid(p.hToolTip)
    % Adjust magnitude by dragging axis
    if changingLowerLim
        str = 'Drag to change LOWER magnitude limit';
    else
        str = {'Drag to change UPPER magnitude limit', ...
            'Drag along circle to change axis angle'};
    end
    start(p.hToolTip,str);
end
end

function showToolTipAndPtr_buttondown(p)

brkpt = getMagTickHoverBrkpt(p);
pt = p.hAxes.CurrentPoint;
r = norm(pt(1,1:2));
lowerLim = r < brkpt;
if lowerLim
    str = 'Drag radially to change LOWER limit';
else
    str = { ...
        'Drag radially to change UPPER limit', ...
        'Drag to rotate axis angle'};
end
if p.ToolTips && ~isempty(p.hToolTip) && isvalid(p.hToolTip)
    start(p.hToolTip,str);
end
ptr = 'closedhand';
setptr(p.hFigure,ptr);
end
