//
// SpinnerButton.cpp : implementation file
//
/////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2000 Mikko Mononen <memon@inside.org>
// Feel free to use this file 
//
// This file implements a spinner button control similar
// to the one in 3D Studio MAX. Spinner can automatically
// update it's value to any control (buddy). Output value
// can be integer or double. Internally value is handled in
// floating-point.User can use two arrow buttons to increment
// and decrement the value or use mouse wheel or adjust the
// value by pressing any of the arrow buttons and moving
// the mouse. CTRL-key combined with any operation scales
// the increment by factor of preset multiplier. Value clampping
// can be enabed or disabled and minumum and maximum values
// can be set.
//

#pragma warning( disable : 4786 )		// long names generated by STL

#include "SpinnerBtnC.h"
#include <windows.h>
#include <stdio.h>
#include <math.h>		// fabs



bool SpinnerBtnC::m_bClassRegistered = false;
static LRESULT CALLBACK stubWindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );

/////////////////////////////////////////////////////////////////////////////
// SpinnerBtnC

SpinnerBtnC::SpinnerBtnC()
{
	m_hWnd = 0;
	m_hWndBuddy = 0;
	m_bUp = FALSE;
	m_bDown = FALSE;
	m_nTimer = 0;
	m_bAdjusting = FALSE;
	m_hOldCursor = 0;
	m_dStarty = 0;
	m_nFlags = SPNB_SETBUDDYFLOAT | SPNB_USELIMITS;
	m_dStartValue = 0;
	m_dValue = 0;
	m_dScale = 0.1;
	m_dMultiplier = 10;
	m_dMin = 0;
	m_dMax = 100;
	m_bNegScale = FALSE;
	m_nTimerCount = 0;
}

SpinnerBtnC::~SpinnerBtnC()
{
	if( m_hWnd )
		DestroyWindow( m_hWnd );

//	Cleanup();
}


bool SpinnerBtnC::RegisterClass( HINSTANCE hInstance )
{
	WNDCLASS wc;

	wc.style = 0;
	wc.lpfnWndProc = stubWindowProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = hInstance;
	wc.hIcon = NULL;
	wc.hCursor = LoadCursor( NULL, IDC_ARROW );
	wc.hbrBackground = GetSysColorBrush( COLOR_BTNFACE );
	wc.lpszMenuName = NULL;
	wc.lpszClassName = "mpdSpinnerClass";

	return ::RegisterClass( &wc ) != FALSE;
}

void
SpinnerBtnC::MakeFormatString()
{
	_snprintf( m_szFormatStr, 31, "%%.%df", (int)__max( -floor( log10( m_dScale ) ), 0 ) );
}

BOOL SpinnerBtnC::Create( HINSTANCE hInstance, DWORD dwStyle, const RECT& rect, HWND hParentWnd, UINT nID ) 
{

	if( !m_bClassRegistered ) {
		if(! RegisterClass( hInstance ) )
			return FALSE;
		m_bClassRegistered = true;
	}

    // Register and create the window.
	m_nFlags = dwStyle & SPNB_ALL;

	dwStyle &= ~SPNB_ALL;	// Clear SpinnerButton styles.


	m_hWnd = CreateWindow( "mpdSpinnerClass", "", dwStyle, rect.left,
							rect.top,
							rect.right - rect.left,
							rect.bottom - rect.top,
							hParentWnd, (HMENU)nID, hInstance, (LPVOID)this );

	CreateObjects();
	MakeFormatString();

	return TRUE;
}



void SpinnerBtnC::CreateObjects()
{
  m_hBlackPen = CreatePen( PS_SOLID, 1, GetSysColor( COLOR_BTNTEXT ) );
  m_hBlackBrush = CreateSolidBrush( GetSysColor( COLOR_BTNTEXT ) );
}

void SpinnerBtnC::DeleteObjects()
{
    DeleteObject( m_hBlackPen );
    DeleteObject( m_hBlackBrush );
	m_hBlackPen = 0;
	m_hBlackBrush = 0;
}

void SpinnerBtnC::OnPaint( HWND hWnd, HDC hDC ) 
{
	HBRUSH	hOldBrush;
	HPEN	hOldPen;

	hOldPen = (HPEN)SelectObject( hDC, (HGDIOBJ)m_hBlackPen );
	hOldBrush = (HBRUSH)SelectObject( hDC, (HGDIOBJ)m_hBlackBrush );

	// Boxes
	DrawEdge( hDC, &m_rUpRect, m_bUp ? BDR_SUNKENINNER : BDR_RAISEDINNER, BF_RECT );
	DrawEdge( hDC, &m_rDownRect, m_bDown ? BDR_SUNKENINNER : BDR_RAISEDINNER, BF_RECT );

	POINT	pt[3];

	// Up arrow
	int	iXBase = (m_bUp ? 1 : 0) + m_rUpRect.right / 2;
	pt[0].x = iXBase - m_nArrowSize;
	pt[1].x = iXBase;
	pt[2].x = iXBase + m_nArrowSize;

	int	iYBase = (m_bUp ? 1 : 0) + m_rUpRect.bottom - m_rUpRect.bottom / 3 - 1;	// bottom value is not included in the rect so we always go off by one.
	pt[0].y = iYBase;
	pt[1].y = iYBase - m_nArrowSize;
	pt[2].y = iYBase;

	Polygon( hDC, pt, 3 );

	// Down Arrow
	iXBase = (m_bDown ? 1 : 0) + m_rDownRect.right / 2;
	pt[0].x = iXBase - m_nArrowSize;
	pt[1].x = iXBase;
	pt[2].x = iXBase + m_nArrowSize;

	iYBase = (m_bDown ? 1 : 0) + m_rDownRect.top + (m_rDownRect.bottom - m_rDownRect.top) / 3;
	pt[0].y = iYBase;
	pt[1].y = iYBase + m_nArrowSize;
	pt[2].y = iYBase;

	Polygon( hDC, pt, 3 );

	SelectObject( hDC, hOldPen );
	SelectObject( hDC, hOldBrush );
}

void SpinnerBtnC::OnLButtonDown( UINT nFlags, POINT point )
{
	m_bAdjusting = FALSE;
	m_nTimerCount = 0;

	if( point.y < m_rUpRect.bottom ) {
		// User pressed up-button.
		m_bUp = TRUE;
		m_bDown = FALSE;
		m_nTimer = SetTimer( m_hWnd, 1, 250, NULL );
		UpdateBuddy( FALSE );
	}
	else {
		// User pressed down-button.
		m_bUp = FALSE;
		m_bDown = TRUE;
		m_nTimer = SetTimer( m_hWnd, 1, 250, NULL );
		UpdateBuddy( FALSE );
	}

	// Save inital values
	m_dStarty = (double)point.y;
	m_dStartValue = m_dValue;

	InvalidateRect( m_hWnd, NULL, TRUE );
	SetCapture( m_hWnd );
}

void SpinnerBtnC::OnMouseMove( UINT nFlags, POINT point ) 
{
	if( (nFlags & MK_LBUTTON) && (GetCapture() == m_hWnd) ) {

		// Kill the timer if it exists
		if( m_nTimer )
			KillTimer( m_hWnd, m_nTimer );

		double	dMultiplier = 1.0;
		if( nFlags & MK_CONTROL )
			dMultiplier = m_dMultiplier;

		// If we move mouse while pressed, draw both buttons down.
		if( !m_bUp || !m_bDown ) {
			m_bUp = TRUE;
			m_bDown = TRUE;
			InvalidateRect( m_hWnd, NULL, TRUE );
		}
		m_bAdjusting = TRUE;
		m_hOldCursor = SetCursor( LoadCursor( NULL, IDC_SIZENS ) );
		m_dValue = m_dStartValue - ((double)point.y - m_dStarty) * m_dScale * dMultiplier;
		UpdateBuddy( TRUE );
	}
}

void SpinnerBtnC::OnLButtonUp( UINT nFlags, POINT point ) 
{
	if( m_nTimer )
		KillTimer( m_hWnd, m_nTimer );

	if( GetCapture() == m_hWnd ) {

		if( !m_nTimerCount && !m_bAdjusting ) {
			// User pressed button down, but no timer events occured yet nor
			// did the user use the adjusting mode (move mouse). Thread this case
			// as single button press.
			double	dMultiplier = 1.0;
			if( nFlags & MK_CONTROL )
				dMultiplier = m_dMultiplier;

			if( m_bUp )
				m_dValue += m_dScale * dMultiplier;
			else
				m_dValue -= m_dScale * dMultiplier;
		}

		m_bUp = FALSE;
		m_bDown = FALSE;

		if( m_bAdjusting ) {
			SetCursor( m_hOldCursor );
			m_bAdjusting = FALSE;
		}

		InvalidateRect( m_hWnd, NULL, TRUE );
		ReleaseCapture();

		
		if( m_dValue != m_dStartValue && m_hWndBuddy ) {
			// Notify anything that updates its value in WM_KILLFOCUS
			SetFocus( m_hWndBuddy );
		}

		UpdateBuddy( TRUE );

		if( m_dValue != m_dStartValue && m_hWndBuddy ) {
			// Notify anything that updates its value in WM_KILLFOCUS
			SetFocus( m_hWnd );
		}
	}
}

void SpinnerBtnC::OnSize( UINT nType, int cx, int cy ) 
{
	int	width = !(cx & 1) ? cx - 1 : cx;
	int	height = cy / 2;

	m_nArrowSize = (width > height ? height : width) / 3;

	m_rUpRect.left = 0;
	m_rUpRect.right = width;
	m_rUpRect.top = 0;
	m_rUpRect.bottom = height;

	m_rDownRect.left = 0;
	m_rDownRect.right = width;
	m_rDownRect.top = cy - height;
	m_rDownRect.bottom = cy;

}

void SpinnerBtnC::OnTimer( UINT nIDEvent ) 
{
	// Timer is set on when user presses up or down button.
	// Each time timer event occurs value is incremented or
	// decremented depending which button is pressed.
	if( nIDEvent == 1 ) {
		if( m_bUp )
			m_dValue += m_dScale;
		else
			m_dValue -= m_dScale;
		m_dStartValue = m_dValue;
		UpdateBuddy( TRUE );
		m_nTimerCount++;
	}
}

HWND SpinnerBtnC::SetBuddy( HWND hWndBuddy, DWORD nFlags )
{
	HWND	hOld = m_hWndBuddy;
	m_hWndBuddy = hWndBuddy;

	if( hWndBuddy && nFlags & SPNB_ATTACH_LEFT ) {
		// Move spinner left of buddy window
		if( GetParent( m_hWnd ) ) {
			RECT	rect;
			GetWindowRect( hWndBuddy, &rect );

			POINT	pt;
			pt.x = rect.left;
			pt.y = rect.top;
			ScreenToClient( GetParent( m_hWnd ), &pt );
			rect.left = pt.x;
			rect.top = pt.y;

			pt.x = rect.right;
			pt.y = rect.bottom;
			ScreenToClient( GetParent( m_hWnd ), &pt );
			rect.right = pt.x;
			rect.bottom = pt.y;

			rect.left += 1;
			rect.top += 1;
			rect.right -= 1;
			rect.bottom -= 1;
			rect.right = rect.left - 3;
			rect.left = rect.right - ((rect.bottom - rect.top) * 4 / 5);
			MoveWindow( m_hWnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE );
			OnSize( 0, rect.right - rect.left, rect.bottom - rect.top );
		}
	}
	else if( hWndBuddy && nFlags & SPNB_ATTACH_RIGHT ) {
		// Move spinner right of buddy window
		if( GetParent( m_hWnd ) ) {
			RECT	rect;
			GetWindowRect( hWndBuddy, &rect );

			POINT	pt;
			pt.x = rect.left;
			pt.y = rect.top;
			ScreenToClient( GetParent( m_hWnd ), &pt );
			rect.left = pt.x;
			rect.top = pt.y;

			pt.x = rect.right;
			pt.y = rect.bottom;
			ScreenToClient( GetParent( m_hWnd ), &pt );
			rect.right = pt.x;
			rect.bottom = pt.y;

			rect.left += 1;
			rect.top += 1;
			rect.right -= 1;
			rect.bottom -= 1;
			rect.left = rect.right + 3;
			rect.right = rect.left + ((rect.bottom - rect.top) * 4 / 5);
			MoveWindow( m_hWnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE );
			OnSize( 0, rect.right - rect.left, rect.bottom - rect.top );
		}
	}

	return hOld;
}

HWND SpinnerBtnC::GetBuddy() const
{
	return m_hWndBuddy;
}

void SpinnerBtnC::SetScale( double dScale )
{
	m_dScale = fabs( dScale );
	if( m_bNegScale )
		m_dScale = -m_dScale;
	MakeFormatString();
}

void SpinnerBtnC::SetMinMax( double dMin, double dMax )
{
	// If min is larger than max use negative scaling and
	// swap input values for min/max. This allows us to handle
	// values more easily. m_negScale flag is set to be able
	// to assign correct sign to the scale value in SetScale().
	if( dMin > dMax ) {
		m_dMin = dMax;
		m_dMax = dMin;
		m_dScale = -fabs( m_dScale );
		m_bNegScale = TRUE;
	}
	else {
		m_dMin = dMin;
		m_dMax = dMax;
		m_dScale = fabs( m_dScale );
		m_bNegScale = FALSE;
	}
}

double SpinnerBtnC::GetMin() const
{
	return m_bNegScale ? m_dMax : m_dMin;
}

double SpinnerBtnC::GetMax() const
{
	return m_bNegScale ? m_dMin : m_dMax;
}

void SpinnerBtnC::UpdateBuddy( BOOL bSave )
{
	if( bSave ) {
		// Save value to buddy

		// Clamp value.
		if( m_nFlags & SPNB_USELIMITS ) {
			if( m_dValue < m_dMin )
				m_dValue = m_dMin;
			if( m_dValue > m_dMax )
				m_dValue = m_dMax;
		}

		// If buddy set buddy wnd's text.
		if( m_hWndBuddy ) {
			char	szStr[64];
			
			if( m_nFlags & SPNB_SETBUDDYINT ) {
				_snprintf( szStr, 63, "%d", (int)m_dValue );
			}
			else if( m_nFlags & SPNB_SETBUDDYFLOAT ) {
				_snprintf( szStr, 63, m_szFormatStr, m_dValue );
			}
			SetWindowText( m_hWndBuddy, szStr );
		}

	}
	else {
		// Get value from buddy
		if( m_hWndBuddy ) {
			char	szStr[64];
			GetWindowText( m_hWndBuddy, szStr, 63 );
			m_dValue = atof( szStr );

			// Clamp value
			if( m_nFlags & SPNB_USELIMITS ) {
				if( m_dValue < m_dMin )
					m_dValue = m_dMin;
				if( m_dValue > m_dMax )
					m_dValue = m_dMax;
			}
		}
	}
}

void SpinnerBtnC::SetVal( double dVal )
{
	m_dValue = dVal;
}

double SpinnerBtnC::GetVal() const
{
	return m_dValue;
}

void SpinnerBtnC::SetMultiplier( double dVal )
{
	m_dMultiplier = dVal;
}

double SpinnerBtnC::GetMultiplier() const
{
	return m_dMultiplier;
}

static LRESULT CALLBACK stubWindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	SpinnerBtnC*	pClass = 0;
	POINT			pt;
	
	// the usual window procedure...
	
	switch( uMsg )
	{
    case WM_CREATE:
		SetWindowLong( hWnd, GWL_USERDATA, (LONG)((LPCREATESTRUCT)lParam)->lpCreateParams );
		break;
		
    case WM_LBUTTONDOWN:
		pClass = reinterpret_cast<SpinnerBtnC*>(GetWindowLong( hWnd, GWL_USERDATA ) );
		pt.x = (short)LOWORD( lParam );
		pt.y = (short)HIWORD( lParam );
		pClass->OnLButtonDown( wParam, pt );
		break;
		
    case WM_LBUTTONUP:
		pClass = reinterpret_cast<SpinnerBtnC*>(GetWindowLong( hWnd, GWL_USERDATA ) );
		pt.x = (short)LOWORD( lParam );
		pt.y = (short)HIWORD( lParam );
		pClass->OnLButtonUp( wParam, pt );
		break;
		
    case WM_MOUSEMOVE:
		pClass = reinterpret_cast<SpinnerBtnC*>(GetWindowLong( hWnd, GWL_USERDATA ) );
		pt.x = (short)LOWORD( lParam );
		pt.y = (short)HIWORD( lParam );
		pClass->OnMouseMove( wParam, pt );
		break;
		
    case WM_TIMER:
		pClass = reinterpret_cast<SpinnerBtnC*>(GetWindowLong( hWnd, GWL_USERDATA ) );
		pClass->OnTimer( wParam );
		break;
		
    case WM_PAINT:
		{
			PAINTSTRUCT ps;
			
			BeginPaint( hWnd,&ps );
			pClass = reinterpret_cast<SpinnerBtnC*>(GetWindowLong( hWnd, GWL_USERDATA ) );
			pClass->OnPaint( hWnd, ps.hdc );
			EndPaint( hWnd,&ps );
		}
		break;
	
	}
	
	return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
