/***************************************************************************
                          kpoti.cpp  -  potentiometer widget
                             -------------------
    begin                : Wed Apr 28 23:05:05 MEST 1999

    copyright            : (C) 1999 by Martin Lorenz
    email                : lorenz@ch.tum.de
                           (C) 2002-2003 Matthias Kretz <kretz@kde.org>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "kpoti.h"
#include "kpoti.moc"

#include <qbitmap.h>
#include <qpainter.h>
#include <qcolor.h>
#include <qdrawutil.h>
#include <qtimer.h>
#include <qkeycode.h>
#include <qpen.h>
#include <qstring.h>
#include <qstyle.h>

#include <math.h>

#include <kpixmap.h>
#include <kpixmapeffect.h>
#include <kglobal.h>
#include <kdebug.h>
#include <kapplication.h>

#define PI 3.1415926
static const int thresholdTime = 500;
static const int repeatTime    = 100;
static const float maxAngle = PI*135/180; // 140 degrees to both sides
static const float tickLength = 3;

struct KPoti::KPotiPrivate
{
	KPotiPrivate()
		: bgDirty( false )
		, potiDirty( false )
	{}

	bool bgDirty;
	KPixmap bgdb;
	KPixmap bgPixmap( const QColorGroup & colorGroup )
	{
		if( bgDirty || bgdb.isNull() )
		{
			bgdb.resize( buttonRect.size() );
			QPainter dbp( &bgdb );
			dbp.setPen( Qt::NoPen );
			QRect drawRect = bgdb.rect();

			// create mask
			QBitmap mask( bgdb.size(), true );
			QPainter maskpainter( &mask );
			maskpainter.setPen( Qt::NoPen );
			maskpainter.setBrush( Qt::color1 );
			maskpainter.drawEllipse( drawRect );
			maskpainter.end();
			bgdb.setMask( mask );

			// inset shadow
			KPixmap gradient( bgdb.size() );
			KPixmapEffect::gradient( gradient, colorGroup.light(), colorGroup.dark(), KPixmapEffect::DiagonalGradient );
			dbp.setBrush( QBrush( colorGroup.button(), gradient ) );
			dbp.drawEllipse( drawRect );

			potiRect.setSize( drawRect.size() * 0.9 );
			if( potiRect.width() + 6 > drawRect.width() )
			{
				potiRect.setWidth( drawRect.width() - 6 );
				potiRect.setHeight( drawRect.height() - 6 );
			}
			potiRect.moveCenter( center );

			bgDirty = false;
		}
		return bgdb;
	}

	QColor potiColor;
	bool potiDirty;
	KPixmap potidb;
	KPixmap potiPixmap()
	{
		if( ( potiDirty || potidb.isNull() ) && ! potiRect.size().isEmpty() )
		{
			potidb.resize( potiRect.size() );
			QPainter dbp( &potidb );
			dbp.setPen( Qt::NoPen );
			QRect drawRect( potidb.rect() );

			// create mask
			QBitmap mask( potidb.size(), true );
			QPainter maskpainter( &mask );
			maskpainter.setPen( Qt::NoPen );
			maskpainter.setBrush( Qt::color1 );
			maskpainter.drawEllipse( drawRect );
			maskpainter.end();
			potidb.setMask( mask );

			KPixmap gradient( potidb.size() );
			KPixmapEffect::gradient( gradient, potiColor.dark( 130 ), potiColor.light( 130 ), KPixmapEffect::DiagonalGradient );
			dbp.setBrush( QBrush( potiColor, gradient ) );
			dbp.drawEllipse( drawRect );

			potiDirty = false;
		}
		return potidb;
	}

	QRect buttonRect;
	QRect potiRect;
	QRect labelRect;
	QString label;
	QPoint center;
};

QSizePolicy KPoti::sizePolicy() const
{
	return QSizePolicy( QSizePolicy::Preferred, QSizePolicy::Preferred );
}

QSize KPoti::sizeHint() const
{
	return minimumSizeHint();
}

QSize KPoti::minimumSizeHint() const
{
	int width = 40;
	int height = 40;
	if( m_bLabel )
	{
		QFontMetrics metrics( font() );
		d->labelRect = metrics.boundingRect( d->label );
		d->labelRect.setHeight( metrics.lineSpacing() );
		width = KMAX( width, d->labelRect.width() + frameRect().width() - contentsRect().width() );
		height += metrics.lineSpacing();
	}
	//kdDebug() << k_funcinfo << "return " <<  width << "x" << height << endl;
	return QSize( width, height );
}

QString KPoti::text() const
{
	return d->label;
}

void KPoti::setText( const QString & text )
{
	d->label = text;
	setMinimumSize( minimumSizeHint() );
	updateGeometry();
}

/**
  Constructs a poti.

  The \e parent and \e name arguments are sent to the QWidget constructor.
*/
KPoti::KPoti( QWidget *parent, const char *name )
    : QFrame( parent, name, WResizeNoErase | WRepaintNoErase )
	, d( 0 )
{
    init();
}



/**
  Constructs a poti.

  \arg \e minValue is the minimum slider value.
  \arg \e maxValue is the maximum slider value.
  \arg \e step is the page step value.
  \arg \e value is the initial value.

  The \e parent and \e name arguments are sent to the QWidget constructor.
*/

KPoti::KPoti( int minValue, int maxValue, int step,
		  int value, QWidget *parent, const char *name )
    : QFrame( parent, name, WResizeNoErase | WRepaintNoErase )
	, QRangeControl( minValue, maxValue, 1, step, value )
	, d( 0 )
{
    init(value);
}

KPoti::~KPoti()
{
	delete d;
	d = 0;
}

void KPoti::init(int value)
{
	d = new KPotiPrivate;
	font().setPointSize( 8 );
	d->potiColor.setNamedColor( "red" );

  timer = 0;
  potiVal = value;
  potiPos = positionFromValue(value);
  clickOffset = 0;
  state = Idle;
  track = TRUE;
  ticks = TRUE;
  m_bLabel = true;
  tickInt = 0;

  setFocusPolicy( TabFocus );
  initTicks();
}


/**
  Does what's needed when someone changes the tickmark status
*/

void KPoti::initTicks()
{
	QRect available = contentsRect();
	if( m_bLabel )
		available.rTop() += d->labelRect.height();
	d->center = available.center();
	// make the width and height equal
	if( available.width() > available.height() )
		available.setWidth( available.height() );
	else if( available.height() > available.width() )
		available.setHeight( available.width() );
	available.moveCenter( d->center );
	d->buttonRect = available;

	buttonRadius = available.width() / 2.0;
	if( ticks )
	{
		buttonRadius -= tickLength;
		int tickSpace = static_cast<int>( tickLength );
		d->buttonRect.rTop() += tickSpace;
		d->buttonRect.rLeft() += tickSpace;
		d->buttonRect.rRight() -= tickSpace;
		d->buttonRect.rBottom() -= tickSpace;
	}

	d->potiDirty = true;
	d->bgDirty = true;
}


/**
  Enables slider tracking if \e enable is TRUE, or disables tracking
  if \e enable is FALSE.

  If tracking is enabled (default), the slider emits the
  valueChanged() signal whenever the slider is being dragged.  If
  tracking is disabled, the slider emits the valueChanged() signal
  when the user releases the mouse button (unless the value happens to
  be the same as before).

  \sa tracking()
*/

void KPoti::setTracking( bool enable )
{
    track = enable;
}


/**
  \fn bool KPoti::tracking() const
  Returns TRUE if tracking is enabled, or FALSE if tracking is disabled.

  Tracking is initially enabled.

  \sa setTracking()
*/


/**
  \fn void KPoti::valueChanged( int value )
  This signal is emitted when the slider value is changed, with the
  new slider value as an argument.
*/

/**
  \fn void KPoti::sliderPressed()
  This signal is emitted when the user presses the slider with the mouse.
*/

/**
  \fn void KPoti::sliderMoved( int value )
  This signal is emitted when the slider is dragged, with the
  new slider value as an argument.
*/

/**
  \fn void KPoti::sliderReleased()
  This signal is emitted when the user releases the slider with the mouse.
*/

/**
  Calculates slider position corresponding to value \a v. Does not perform
  rounding.
*/

float KPoti::positionFromValue( int v ) const
{

    int range = maxValue() - minValue();
    return ( (v - minValue() ) *2* maxAngle) / range - maxAngle;
}


/**
  Calculates value corresponding to poti position \a p. Performs rounding.
*/

int KPoti::valueFromPosition( float p ) const
{
    int range = maxValue() - minValue();
    return (int) (minValue() + ((p+maxAngle)*range)/(2*maxAngle));
}

/*!
  Implements the virtual QRangeControl function.
*/

void KPoti::rangeChange()
{
	float newPos = positionFromValue( value() );
	if ( newPos != potiPos ) {
		reallyMovePoti( newPos );
	}
}

void KPoti::paletteChange( const QPalette & )
{
	d->bgDirty = true;
	d->potiDirty = true;
}

/*!
  Changes the value (slot)
*/

void KPoti::valueChange()
{
    if ( potiVal != value() ) {
	float newPos = positionFromValue( value() );
	potiVal = value();
	reallyMovePoti( newPos );
    }
    emit valueChanged(value());
}


/*!
  Handles resize events for the poti.
*/

void KPoti::resizeEvent( QResizeEvent * )
{
    rangeChange();
    initTicks();
}

void KPoti::setLabel(bool s)
{
	m_bLabel = s;
	initTicks();
}

/**
  Sets the color of the button
  */
void KPoti::setColor( const QColor &c )
{
	d->potiColor = c;
	d->potiDirty = true;
	repaint();
}


void KPoti::paintPoti( QPainter * p )
{
	if( isVisible() )
	{
		KPixmap db = d->potiPixmap();
		if( db.isNull() )
			return;

		QPainter p2( &db );
		p2.translate( db.rect().center().x(), db.rect().center().y() );
		p2.rotate( potiPos * 180.0 / PI );
		QRect pointer( db.width() / -20, db.width() / -2, db.width() / 10, db.width() / 2 );
		QBrush buttonbrush( colorGroup().button() );
		qDrawShadePanel( &p2, pointer, colorGroup(), true, 1, &buttonbrush );
		p2.end();

		p->drawPixmap( d->potiRect, db );
	}
}

/*!
  Performs the actual moving of the slider.
*/

void KPoti::reallyMovePoti( float newPos )
{
  QPainter p;
  p.begin( this );
  p.setPen(NoPen);
  potiPos = newPos;
  paintPoti(&p);
  p.end();
}




/**
  Handles paint events for the slider.
*/

void KPoti::drawContents( QPainter * p )
{
	QPixmap doublebuffer( contentsRect().size() );
	doublebuffer.fill( colorGroup().background() );
	QPainter dbp( &doublebuffer );
	if( m_bLabel )
	{
		dbp.setFont( font() );
		QFontMetrics metrics = dbp.fontMetrics();
		dbp.drawText( contentsRect().x() - metrics.leftBearing( d->label[ 0 ] ) + ( contentsRect().width() - d->labelRect.width() ) / 2, metrics.height(), d->label );
	}

    int interval = tickInt;
    if( interval <= 0 )
		interval = 12;
    if( ticks )
		drawTicks( &dbp, buttonRadius, tickLength, interval );

	dbp.drawPixmap( d->buttonRect, d->bgPixmap( colorGroup() ) );

	if( hasFocus() )
		style().drawPrimitive( QStyle::PE_FocusRect, &dbp, d->buttonRect, colorGroup() );

    paintPoti( &dbp );
	dbp.end();
	p->drawPixmap( contentsRect(), doublebuffer );
}


/*!
  Handles mouse press events for the slider.
*/

void KPoti::mousePressEvent( QMouseEvent *e )
{
  resetState();

  if ( e->button() == MidButton ) {
    double pos = atan2( double(e->pos().x()-d->center.x()),
                       double(- e->pos().y() + d->center.y()) );
    movePoti( pos );
    return;
  }
  if ( e->button() != LeftButton )
    return;


  int dx=e->pos().x()-d->center.x(), dy=e->pos().y()-d->center.y();

  if ( dx*dx+dy*dy < buttonRadius*buttonRadius ) {
    state = Dragging;
    clickOffset =  potiVal + (e->pos().y() ) ;
    emit potiPressed();
  }  else if ( e->pos().x() < width()/2 ) {
    state = TimingDown;
    subtractPage();
    if ( !timer )
      timer = new QTimer( this );
    connect( timer, SIGNAL(timeout()), SLOT(repeatTimeout()) );
    timer->start( thresholdTime, TRUE );
  } else  {
    state = TimingUp;
    addPage();
    if ( !timer )
      timer = new QTimer( this );
    connect( timer, SIGNAL(timeout()), SLOT(repeatTimeout()) );
    timer->start( thresholdTime, TRUE );
  }
}

/*!
  Handles mouse move events for the slider.
*/
void KPoti::mouseMoveEvent( QMouseEvent *e )
{

    if ( (e->state() & MidButton) ) { 		// middle button wins
      double pos = atan2( double(e->pos().x()-d->center.x()),
                          double(- e->pos().y()+d->center.y()) );
      movePoti( pos );
      return;
    }
    if ( !(e->state() & LeftButton) )
	return;					// left mouse button is up
    if ( state != Dragging )
	return;


    movePoti( positionFromValue(- e->pos().y() + clickOffset ));
}


/*!
  Handles mouse release events for the slider.
*/

void KPoti::mouseReleaseEvent( QMouseEvent *e )
{
    if ( !(e->button() & LeftButton) )
	return;
    resetState();
}

void KPoti::focusInEvent( QFocusEvent * e )
{
	//setFrameStyle( Raised | Box );
	//setLineWidth( 1 );
	QFrame::focusInEvent( e );
}

void KPoti::focusOutEvent( QFocusEvent * e )
{
	//setFrameStyle( NoFrame );
	//setLineWidth( 0 );
	QFrame::focusOutEvent( e );
}


void KPoti::enterEvent( QEvent * )
{
    emit mouseEntered( potiVal );
}


/*!
  Moves the left (or top) edge of the slider to position
  \a pos. Performs snapping.
*/

void KPoti::movePoti( float pos )
{
    float newPos = QMIN( maxAngle, QMAX( -maxAngle, pos ) );
    int newVal = valueFromPosition( newPos );
    if ( potiVal != newVal ) {
	potiVal = newVal;
	emit potiMoved( potiVal );
    }
    if ( tracking() && potiVal != value() ) {
	directSetValue( potiVal );
	emit valueChanged( potiVal );
    }


    if ( potiPos != newPos )
	reallyMovePoti( newPos );
}


/*!
  Resets all state information and stops my timer.
*/

void KPoti::resetState()
{
    if ( timer ) {
	timer->stop();
	timer->disconnect();
    }
    switch ( state ) {
    case TimingUp:
    case TimingDown:
	break;
    case Dragging: {
	setValue( valueFromPosition( potiPos ) );
	emit potiReleased();
	break;
    }
    case Idle:
	break;
    default:
	kdWarning() << "KPoti: in wrong state" << endl;
    }
    state = Idle;
}


/*!
  Handles key press events for the slider.
*/

void KPoti::keyPressEvent( QKeyEvent *e )
{

    switch ( e->key() ) {
    case Key_Left:
      subtractLine();
      break;
    case Key_Right:
      addLine();
      break;
    case Key_Up:
      addLine();
      break;
    case Key_Down:
      subtractLine();
      break;
    case Key_Prior:
      subtractPage();
      break;
    case Key_Next:
      addPage();
      break;
    case Key_Home:
      setValue( minValue() );
      break;
    case Key_End:
      setValue( maxValue() );
      break;
    default:
	e->ignore();
	return;
    }
    e->accept();
}





/*!
  Makes QRangeControl::setValue() available as a slot.
*/

void KPoti::setValue( int value )
{
    QRangeControl::setValue( value );
}


/*!
  Moves the slider one pageStep() upwards.
*/

void KPoti::addStep()
{
    addPage();
}


/*!
  Moves the slider one pageStep() downwards.
*/

void KPoti::subtractStep()
{
    subtractPage();
}


/*!
  Waits for autorepeat.
*/

void KPoti::repeatTimeout()
{
    Q_ASSERT( timer );
    timer->disconnect();
    if ( state == TimingDown )
	connect( timer, SIGNAL(timeout()), SLOT(subtractStep()) );
    else if ( state == TimingUp )
	connect( timer, SIGNAL(timeout()), SLOT(addStep()) );
    timer->start( repeatTime, FALSE );
}




/*!
  Using \a p, draws tickmarks at a distance of \a dist from the edge
  of the widget, using \a w pixels and \a i intervals.
  */

void KPoti::drawTicks( QPainter *p, double dist, double w, int i ) const
{
	p->setPen( colorGroup().foreground() );
	double angle,s,c;
	double x, y;
	for (int v=0; v<=i; v++)
	{
		angle = -maxAngle+2*maxAngle*v/i;
		s = sin( angle );
		c = cos( angle );
		x = d->center.x() - s * dist;
		y = d->center.y() - c * dist;

		p->drawLine( (int)x, (int)y, (int)(x - s * w), (int)(y - c * w) );
	}
}

void KPoti::wheelEvent(QWheelEvent *e)
{
	setValue(value()+e->delta()/120*8);
}


/*!
  Sets the way tickmarks are displayed by the slider. \a s can take
  the following values:
  <ul>
  <li> \c NoMarks
  <li> \c Above
  <li> \c Left
  <li> \c Below
  <li> \c Right
  <li> \c Both
  </ul>
  The initial value is \c NoMarks.
  \sa tickmarks(), setTickInterval()
*/

void KPoti::setTickmarks( bool s )
{
    ticks = s;
    initTicks();
    update();
}



/*!
  Sets the interval between tickmarks to \a i. This is a value interval,
  not a pixel interval. If \a i is 0, the slider
  will choose between lineStep() and pageStep(). The initial value of
  tickInterval() is 0.
  \sa tickInterval(), QRangeControl::lineStep(), QRangeControl::pageStep()
*/

void KPoti::setTickInterval( int i )
{
    tickInt = QMAX( 0, i );
    update();
}


/*!
  \fn int KPoti::tickInterval() const
  Returns the interval between tickmarks. Returns 0 if the slider
  chooses between pageStep() and lineStep().
  \sa setTickInterval()
*/

// vim: sw=4 ts=4
