/*
 *  spinbox.cpp  -  spin box with read-only option and shift-click step value
 *  Program:  kalarm
 *  Copyright © 2002,2004,2008 by David Jarvie <djarvie@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.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <tqlineedit.h>
#include <tqobjectlist.h>
#include "spinbox.moc"


SpinBox::SpinBox(TQWidget* parent, const char* name)
	: TQSpinBox(0, 99999, 1, parent, name),
	  mMinValue(TQSpinBox::minValue()),
	  mMaxValue(TQSpinBox::maxValue())
{
	init();
}

SpinBox::SpinBox(int minValue, int maxValue, int step, TQWidget* parent, const char* name)
	: TQSpinBox(minValue, maxValue, step, parent, name),
	  mMinValue(minValue),
	  mMaxValue(maxValue)
{
	init();
}

void SpinBox::init()
{
	int step = TQSpinBox::lineStep();
	mLineStep        = step;
	mLineShiftStep   = step;
	mCurrentButton   = NO_BUTTON;
	mShiftMouse      = false;
	mShiftMinBound   = false;
	mShiftMaxBound   = false;
	mSelectOnStep    = true;
	mReadOnly        = false;
	mSuppressSignals = false;
	mEdited          = false;

	// Find the spin widgets which are part of the spin boxes, in order to
	// handle their shift-button presses.
	TQObjectList* spinwidgets = queryList("TQSpinWidget", 0, false, true);
	TQSpinWidget* spin = (TQSpinWidget*)spinwidgets->getFirst();
	if (spin)
		spin->installEventFilter(this);   // handle shift-button presses
	delete spinwidgets;
	editor()->installEventFilter(this);   // handle shift-up/down arrow presses

	// Detect when the text field is edited
	connect(editor(), TQ_SIGNAL(textChanged(const TQString&)), TQ_SLOT(textEdited()));
}

void SpinBox::setReadOnly(bool ro)
{
	if ((int)ro != (int)mReadOnly)
	{
		mReadOnly = ro;
		editor()->setReadOnly(ro);
		if (ro)
			setShiftStepping(false, mCurrentButton);
	}
}

int SpinBox::bound(int val) const
{
	return (val < mMinValue) ? mMinValue : (val > mMaxValue) ? mMaxValue : val;
}

void SpinBox::setMinValue(int val)
{
	mMinValue = val;
	TQSpinBox::setMinValue(val);
	mShiftMinBound = false;
}

void SpinBox::setMaxValue(int val)
{
	mMaxValue = val;
	TQSpinBox::setMaxValue(val);
	mShiftMaxBound = false;
}

void SpinBox::setLineStep(int step)
{
	mLineStep = step;
	if (!mShiftMouse)
		TQSpinBox::setLineStep(step);
}

void SpinBox::setLineShiftStep(int step)
{
	mLineShiftStep = step;
	if (mShiftMouse)
		TQSpinBox::setLineStep(step);
}

void SpinBox::stepUp()
{
	int step = TQSpinBox::lineStep();
	addValue(step);
	emit stepped(step);
}

void SpinBox::stepDown()
{
	int step = -TQSpinBox::lineStep();
	addValue(step);
	emit stepped(step);
}

/******************************************************************************
* Adds a positive or negative increment to the current value, wrapping as appropriate.
* If 'current' is true, any temporary 'shift' values for the range are used instead
* of the real maximum and minimum values.
*/
void SpinBox::addValue(int change, bool current)
{
	int newval = value() + change;
	int maxval = current ? TQSpinBox::maxValue() : mMaxValue;
	int minval = current ? TQSpinBox::minValue() : mMinValue;
	if (wrapping())
	{
		int range = maxval - minval + 1;
		if (newval > maxval)
			newval = minval + (newval - maxval - 1) % range;
		else if (newval < minval)
			newval = maxval - (minval - 1 - newval) % range;
	}
	else
	{
		if (newval > maxval)
			newval = maxval;
		else if (newval < minval)
			newval = minval;
	}
	setValue(newval);
}

void SpinBox::valueChange()
{
	if (!mSuppressSignals)
	{
		int val = value();
		if (mShiftMinBound  &&  val >= mMinValue)
		{
			// Reinstate the minimum bound now that the value has returned to the normal range.
			TQSpinBox::setMinValue(mMinValue);
			mShiftMinBound = false;
		}
		if (mShiftMaxBound  &&  val <= mMaxValue)
		{
			// Reinstate the maximum bound now that the value has returned to the normal range.
			TQSpinBox::setMaxValue(mMaxValue);
			mShiftMaxBound = false;
		}

		bool focus = !mSelectOnStep && hasFocus();
		if (focus)
			clearFocus();     // prevent selection of the spin box text
		TQSpinBox::valueChange();
		if (focus)
			setFocus();
	}
}

/******************************************************************************
* Called whenever the line edit text is changed.
*/
void SpinBox::textEdited()
{
	mEdited = true;
}

void SpinBox::updateDisplay()
{
	mEdited = false;
	TQSpinBox::updateDisplay();
}

/******************************************************************************
* Receives events destined for the spin widget or for the edit field.
*/
bool SpinBox::eventFilter(TQObject* obj, TQEvent* e)
{
	if (obj == editor())
	{
		int step = 0;
		bool shift = false;
		switch (e->type())
		{
			case TQEvent::KeyPress:
			{
				// Up and down arrow keys step the value
				TQKeyEvent* ke = (TQKeyEvent*)e;
				int key = ke->key();
				if (key == TQt::Key_Up)
					step = 1;
				else if (key == TQt::Key_Down)
					step = -1;
				shift = ((ke->state() & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton);
				break;
			}
			case TQEvent::Wheel:
			{
				TQWheelEvent* we = (TQWheelEvent*)e;
				step = (we->delta() > 0) ? 1 : -1;
				shift = ((we->state() & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton);
				break;
			}
			case TQEvent::Leave:
				if (mEdited)
					interpretText();
				break;
			default:
				break;
		}
		if (step)
		{
			if (mReadOnly)
				return true;    // discard up/down arrow keys or wheel
			if (shift)
			{
				// Shift stepping
				int val = value();
				if (step > 0)
					step = mLineShiftStep - val % mLineShiftStep;
				else
					step = - ((val + mLineShiftStep - 1) % mLineShiftStep + 1);
			}
			else
				step = (step > 0) ? mLineStep : -mLineStep;
			addValue(step, false);
			return true;
		}
	}
	else
	{
		int etype = e->type();    // avoid switch compile warnings
		switch (etype)
		{
			case TQEvent::MouseButtonPress:
			case TQEvent::MouseButtonDblClick:
			{
				TQMouseEvent* me = (TQMouseEvent*)e;
				if (me->button() == TQt::LeftButton)
				{
					// It's a left button press. Set normal or shift stepping as appropriate.
					if (mReadOnly)
						return true;   // discard the event
					mCurrentButton = whichButton(me->pos());
					if (mCurrentButton == NO_BUTTON)
						return true;
					bool shift = (me->state() & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton;
					if (setShiftStepping(shift, mCurrentButton))
						return true;     // hide the event from the spin widget
					return false;    // forward event to the destination widget
				}
				break;
			}
			case TQEvent::MouseButtonRelease:
			{
				TQMouseEvent* me = (TQMouseEvent*)e;
				if (me->button() == TQt::LeftButton  &&  mShiftMouse)
				{
					setShiftStepping(false, mCurrentButton);    // cancel shift stepping
					return false;    // forward event to the destination widget
				}
				break;
			}
			case TQEvent::MouseMove:
			{
				TQMouseEvent* me = (TQMouseEvent*)e;
				if (me->state() & TQt::LeftButton)
				{
					// The left button is down. Track which spin button it's in.
					if (mReadOnly)
						return true;   // discard the event
					int newButton = whichButton(me->pos());
					if (newButton != mCurrentButton)
					{
						// The mouse has moved to a new spin button.
						// Set normal or shift stepping as appropriate.
						mCurrentButton = newButton;
						bool shift = (me->state() & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton;
						if (setShiftStepping(shift, mCurrentButton))
							return true;     // hide the event from the spin widget
					}
					return false;    // forward event to the destination widget
				}
				break;
			}
			case TQEvent::Wheel:
			{
				TQWheelEvent* we = (TQWheelEvent*)e;
				bool shift = (we->state() & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton;
				if (setShiftStepping(shift, (we->delta() > 0 ? UP : DOWN)))
					return true;     // hide the event from the spin widget
				return false;    // forward event to the destination widget
			}
			case TQEvent::KeyPress:
			case TQEvent::KeyRelease:
			case TQEvent::AccelOverride:      // this is needed to receive Shift presses!
			{
				TQKeyEvent* ke = (TQKeyEvent*)e;
				int key   = ke->key();
				int state = ke->state();
				if ((state & TQt::LeftButton)
				&&  (key == TQt::Key_Shift  ||  key == TQt::Key_Alt))
				{
					// The left mouse button is down, and the Shift or Alt key has changed
					if (mReadOnly)
						return true;   // discard the event
					state ^= (key == TQt::Key_Shift) ? TQt::ShiftButton : TQt::AltButton;    // new state
					bool shift = (state & (TQt::ShiftButton | TQt::AltButton)) == TQt::ShiftButton;
					if ((!shift && mShiftMouse)  ||  (shift && !mShiftMouse))
					{
						// The effective shift state has changed.
						// Set normal or shift stepping as appropriate.
						if (setShiftStepping(shift, mCurrentButton))
							return true;     // hide the event from the spin widget
					}
				}
				break;
			}
		}
	}
	return TQSpinBox::eventFilter(obj, e);
}

/******************************************************************************
* Set spin widget stepping to the normal or shift increment.
*/
bool SpinBox::setShiftStepping(bool shift, int currentButton)
{
	if (currentButton == NO_BUTTON)
		shift = false;
	if (shift  &&  !mShiftMouse)
	{
		/* The value is to be stepped to a multiple of the shift increment.
		 * Adjust the value so that after the spin widget steps it, it will be correct.
		 * Then, if the mouse button is held down, the spin widget will continue to
		 * step by the shift amount.
		 */
		int val = value();
		int step = (currentButton == UP) ? mLineShiftStep : (currentButton == DOWN) ? -mLineShiftStep : 0;
		int adjust = shiftStepAdjustment(val, step);
		mShiftMouse = true;
		if (adjust)
		{
			/* The value is to be stepped by other than the shift increment,
			 * presumably because it is being set to a multiple of the shift
			 * increment. Achieve this by making the adjustment here, and then
			 * allowing the normal step processing to complete the job by
			 * adding/subtracting the normal shift increment.
			 */
			if (!wrapping())
			{
				// Prevent the step from going past the spinbox's range, or
				// to the minimum value if that has a special text unless it is
				// already at the minimum value + 1.
				int newval = val + adjust + step;
				int svt = specialValueText().isEmpty() ? 0 : 1;
				int minval = mMinValue + svt;
				if (newval <= minval  ||  newval >= mMaxValue)
				{
					// Stepping to the minimum or maximum value
					if (svt  &&  newval <= mMinValue  &&  val == mMinValue)
						newval = mMinValue;
					else
						newval = (newval <= minval) ? minval : mMaxValue;
					TQSpinBox::setValue(newval);
					emit stepped(step);
					return true;
				}

				// If the interim value will lie outside the spinbox's range,
				// temporarily adjust the range to allow the value to be set.
				int tempval = val + adjust;
				if (tempval < mMinValue)
				{
					TQSpinBox::setMinValue(tempval);
					mShiftMinBound = true;
				}
				else if (tempval > mMaxValue)
				{
					TQSpinBox::setMaxValue(tempval);
					mShiftMaxBound = true;
				}
			}

			// Don't process changes since this new value will be stepped immediately
			mSuppressSignals = true;
			bool blocked = signalsBlocked();
			blockSignals(true);
			addValue(adjust, true);
			blockSignals(blocked);
			mSuppressSignals = false;
		}
		TQSpinBox::setLineStep(mLineShiftStep);
	}
	else if (!shift  &&  mShiftMouse)
	{
		// Reinstate to normal (non-shift) stepping
		TQSpinBox::setLineStep(mLineStep);
		TQSpinBox::setMinValue(mMinValue);
		TQSpinBox::setMaxValue(mMaxValue);
		mShiftMinBound = mShiftMaxBound = false;
		mShiftMouse = false;
	}
	return false;
}

/******************************************************************************
* Return the initial adjustment to the value for a shift step up or down.
* The default is to step up or down to the nearest multiple of the shift
* increment, so the adjustment returned is for stepping up the decrement
* required to round down to a multiple of the shift increment <= current value,
* or for stepping down the increment required to round up to a multiple of the
* shift increment >= current value.
* This method's caller then adjusts the resultant value if necessary to cater
* for the widget's minimum/maximum value, and wrapping.
* This should really be a static method, but it needs to be virtual...
*/
int SpinBox::shiftStepAdjustment(int oldValue, int shiftStep)
{
	if (oldValue == 0  ||  shiftStep == 0)
		return 0;
	if (shiftStep > 0)
	{
		if (oldValue >= 0)
			return -(oldValue % shiftStep);
		else
			return (-oldValue - 1) % shiftStep + 1 - shiftStep;
	}
	else
	{
		shiftStep = -shiftStep;
		if (oldValue >= 0)
			return shiftStep - ((oldValue - 1) % shiftStep + 1);
		else
			return (-oldValue) % shiftStep;
	}
}

/******************************************************************************
*  Find which spin widget button a mouse event is in.
*/
int SpinBox::whichButton(const TQPoint& pos)
{
	if (upRect().contains(pos))
		return UP;
	if (downRect().contains(pos))
		return DOWN;
	return NO_BUTTON;
}
