⛏️ index : haiku.git

/*
 * Copyright 2006-2013 Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Stephan Aßmus, superstippi@gmx.de
 *		John Scipione, jscipione@gmail.com
 */


#include "ExpressionTextView.h"

#include <new>
#include <stdio.h>

#include <Beep.h>
#include <ControlLook.h>
#include <Window.h>

#include "CalcView.h"


using std::nothrow;

static const int32 kMaxPreviousExpressions = 20;


ExpressionTextView::ExpressionTextView(BRect frame, CalcView* calcView)
	:
	InputTextView(frame, "expression text view",
		(frame.OffsetToCopy(B_ORIGIN)).InsetByCopy(2, 2),
		B_FOLLOW_NONE, B_WILL_DRAW),
	fCalcView(calcView),
	fKeypadLabels(""),
	fPreviousExpressions(20),
	fHistoryPos(0),
	fCurrentExpression(""),
	fCurrentValue(""),
	fChangesApplied(false)
{
	SetStylable(false);
	SetDoesUndo(true);
	SetColorSpace(B_RGB32);
	SetFontAndColor(be_bold_font, B_FONT_ALL);
	SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
	SetAlignment(B_ALIGN_RIGHT);
}


ExpressionTextView::~ExpressionTextView()
{
	int32 count = fPreviousExpressions.CountItems();
	for (int32 i = 0; i < count; i++)
		delete (BString*)fPreviousExpressions.ItemAtFast(i);
}


void
ExpressionTextView::MakeFocus(bool focused)
{
	if (focused == IsFocus()) {
		// stop endless loop when CalcView calls us again
		return;
	}

	// NOTE: order of lines important!
	InputTextView::MakeFocus(focused);
	fCalcView->MakeFocus(focused);
}


void
ExpressionTextView::KeyDown(const char* bytes, int32 numBytes)
{
	// Handle expression history
	if (bytes[0] == B_UP_ARROW) {
		PreviousExpression();
		return;
	}
	if (bytes[0] == B_DOWN_ARROW) {
		NextExpression();
		return;
	}
	BString current = Text();

	// Handle in InputTextView, except B_TAB
	if (bytes[0] == '=')
		ApplyChanges();
	else if (bytes[0] != B_TAB)
		InputTextView::KeyDown(bytes, numBytes);

	// Pass on to CalcView if this was a label on a key
	if (fKeypadLabels.FindFirst(bytes[0]) >= 0)
		fCalcView->FlashKey(bytes, numBytes);
	else if (bytes[0] == B_BACKSPACE)
		fCalcView->FlashKey("BS", 2);

	// As soon as something is typed, we are at the end of the expression
	// history.
	if (current != Text())
		fHistoryPos = fPreviousExpressions.CountItems();

	// If changes where not applied the value has become a new expression
	// note that even if only the left or right arrow keys are pressed the
	// fCurrentValue string will be cleared.
	if (!fChangesApplied)
		fCurrentValue.SetTo("");
	else
		fChangesApplied = false;
}


void
ExpressionTextView::MouseDown(BPoint where)
{
	uint32 buttons;
	Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons);
	if (buttons & B_PRIMARY_MOUSE_BUTTON) {
		InputTextView::MouseDown(where);
		return;
	}
	where = ConvertToParent(where);
	fCalcView->MouseDown(where);
}


void
ExpressionTextView::GetDragParameters(BMessage* dragMessage,
	BBitmap** bitmap, BPoint* point, BHandler** handler)
{
	InputTextView::GetDragParameters(dragMessage, bitmap, point, handler);
	dragMessage->AddString("be:clip_name", "DeskCalc clipping");
}


void
ExpressionTextView::SetTextRect(BRect rect)
{
	float hInset = floorf(be_control_look->DefaultLabelSpacing() / 2);
	float vInset = floorf((rect.Height() - LineHeight(0)) / 2);
	InputTextView::SetInsets(hInset, vInset, hInset, vInset);
	InputTextView::SetTextRect(rect);

	int32 count = fPreviousExpressions.CountItems();
	if (fHistoryPos == count && fCurrentValue.CountChars() > 0) {
		int32 start;
		int32 finish;
		GetSelection(&start, &finish);
		SetValue(fCurrentValue.String());
		Select(start, finish);
	}
}


// #pragma mark -


void
ExpressionTextView::RevertChanges()
{
	Clear();
}


void
ExpressionTextView::ApplyChanges()
{
	AddExpressionToHistory(Text());
	fCalcView->FlashKey("=", 1);
	fCalcView->Evaluate();
	fChangesApplied = true;
}


// #pragma mark -


void
ExpressionTextView::AddKeypadLabel(const char* label)
{
	fKeypadLabels << label;
}


void
ExpressionTextView::SetExpression(const char* expression)
{
	SetText(expression);
	int32 lastPos = strlen(expression);
	Select(lastPos, lastPos);
}


void
ExpressionTextView::SetValue(BString value, BString decimalSeparator)
{
	// save the value
	fCurrentValue = value;

	// calculate the width of the string
	BFont font;
	uint32 mode = B_FONT_ALL;
	GetFontAndColor(&font, &mode);
	float stringWidth = font.StringWidth(value);

	uint decimalSeparatorWidth = decimalSeparator.CountChars();

	// make the string shorter if it does not fit in the view
	float viewWidth = Frame().Width()
		- floorf(be_control_look->DefaultLabelSpacing() / 2);
	if (value.CountChars() > 3 && stringWidth > viewWidth) {
		// get the position of the first digit
		int32 firstDigit = 0;
		if (value[0] == '-')
			firstDigit++;

		// calculate the value of the exponent
		int32 exponent = 0;
		int32 offset = value.FindFirst(decimalSeparator);
		if (offset == B_ERROR) {
			exponent = value.CountChars() - decimalSeparatorWidth - firstDigit;
			value.InsertChars(decimalSeparator, firstDigit + 1);
		} else {
			if (offset == firstDigit + 1) {
				// if the value is 0.01 or larger then scientific notation
				// won't shorten the string
				if (value[firstDigit] != '0' || value[firstDigit + 2] != '0'
					|| value[firstDigit + 3] != '0') {
					exponent = 0;
				} else {
					// remove the period
					value.Remove(offset, decimalSeparatorWidth);

					// check for negative exponent value
					exponent = 0;
					while (value[firstDigit] == '0') {
						value.Remove(firstDigit, 1);
						exponent--;
					}

					// add the period
					value.InsertChars(decimalSeparator, firstDigit + 1);
				}
			} else {
				// if the period + 1 digit fits in the view scientific notation
				// won't shorten the string
				BString temp = value;
				temp.Truncate(offset + 2);
				stringWidth = font.StringWidth(temp);
				if (stringWidth < viewWidth)
					exponent = 0;
				else {
					// move the period
					value.Remove(offset, decimalSeparatorWidth);
					value.InsertChars(decimalSeparator, firstDigit + 1);

					exponent = offset - (firstDigit + 1);
				}
			}
		}

		if (exponent != 0) {
			value.Truncate(40);
				// truncate to a reasonable precision
				// while ensuring result will be rounded
			offset = value.CountChars() - 1;
			value << "E" << exponent;
				// add the exponent
		} else
			offset = value.CountChars() - 1;

		// reduce the number of digits until the string fits or can not be
		// made any shorter
		stringWidth = font.StringWidth(value);
		char lastRemovedDigit = '0';
		while (offset > firstDigit && stringWidth > viewWidth) {
			if (value.CharAt(offset) != decimalSeparator)
				lastRemovedDigit = value[offset];
			value.Remove(offset--, 1);
			stringWidth = font.StringWidth(value);
		}

		// no need to keep the period if no digits follow
		if (value.CharAt(offset) == decimalSeparator) {
			value.Remove(offset, decimalSeparatorWidth);
			offset--;
		}

		// take care of proper rounding of the result
		int digit = (int)lastRemovedDigit - '0'; // ascii to int
		if (digit >= 5) {
			for (; offset >= firstDigit; offset--) {
				if (value.CharAt(offset) == decimalSeparator)
					continue;

				digit = (int)(value[offset]) - '0' + 1; // ascii to int + 1
				if (digit != 10)
					break;

				value.SetByteAt(offset, '0');
			}
			if (digit == 10) {
				// carry over, shift the result
				if (value.CharAt(firstDigit + 1) == decimalSeparator) {
					value.SetByteAt(firstDigit + decimalSeparatorWidth, '0');
					value.RemoveChars(firstDigit, decimalSeparatorWidth);
					value.InsertChars(decimalSeparator, firstDigit);
				}
				value.Insert('1', 1, firstDigit);

				// remove the exponent value and the last digit
				offset = value.FindFirst('E');
				if (offset == B_ERROR)
					offset = value.CountChars();

				value.Truncate(--offset);
				offset--; // offset now points to the last digit

				// increase the exponent and add it back to the string
				exponent++;
				value << 'E' << exponent;
			} else {
				// increase the current digit value with one
				value.SetByteAt(offset, char(digit + 48));

				// set offset to last digit
				offset = value.FindFirst('E');
				if (offset == B_ERROR)
					offset = value.CountChars();

				offset--;
			}
		}

		// clean up decimal part if we have one
		if (value.FindFirst(decimalSeparator) != B_ERROR) {
			// remove trailing zeros
			while (value[offset] == '0')
				value.Remove(offset--, 1);

			// no need to keep the period if no digits follow
			if (value.CharAt(offset) == decimalSeparator)
				value.Remove(offset, decimalSeparatorWidth);
		}
	}

	// set the new value
	SetExpression(value);
}


void
ExpressionTextView::BackSpace()
{
	const char bytes[1] = { B_BACKSPACE };
	KeyDown(bytes, 1);

	fCalcView->FlashKey("BS", 2);
}


void
ExpressionTextView::Clear()
{
	SetText("");

	fCalcView->FlashKey("C", 1);
}


// #pragma mark -


void
ExpressionTextView::AddExpressionToHistory(const char* expression)
{
	// clean out old expressions that are the same as
	// the one to be added
	int32 count = fPreviousExpressions.CountItems();
	for (int32 i = 0; i < count; i++) {
		BString* item = (BString*)fPreviousExpressions.ItemAt(i);
		if (*item == expression && fPreviousExpressions.RemoveItem(i)) {
			delete item;
			i--;
			count--;
		}
	}

	BString* item = new (nothrow) BString(expression);
	if (!item)
		return;
	if (!fPreviousExpressions.AddItem(item)) {
		delete item;
		return;
	}
	while (fPreviousExpressions.CountItems() > kMaxPreviousExpressions)
		delete (BString*)fPreviousExpressions.RemoveItem((int32)0);

	fHistoryPos = fPreviousExpressions.CountItems();
}


void
ExpressionTextView::PreviousExpression()
{
	int32 count = fPreviousExpressions.CountItems();
	if (fHistoryPos == count) {
		// save current expression
		fCurrentExpression = Text();
	}

	fHistoryPos--;
	if (fHistoryPos < 0) {
		fHistoryPos = 0;
		return;
	}

	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
	if (item != NULL)
		SetExpression(item->String());
}


void
ExpressionTextView::NextExpression()
{
	int32 count = fPreviousExpressions.CountItems();

	fHistoryPos++;
	if (fHistoryPos == count) {
		SetExpression(fCurrentExpression.String());
		return;
	}

	if (fHistoryPos > count) {
		fHistoryPos = count;
		return;
	}

	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
	if (item)
		SetExpression(item->String());
}


// #pragma mark -


void
ExpressionTextView::LoadSettings(const BMessage* archive)
{
	const char* oldExpression;
	for (int32 i = 0;
		archive->FindString("previous expression", i, &oldExpression) == B_OK;
		i++) {
		AddExpressionToHistory(oldExpression);
	}
}


status_t
ExpressionTextView::SaveSettings(BMessage* archive) const
{
	int32 count = fPreviousExpressions.CountItems();
	for (int32 i = 0; i < count; i++) {
		BString* item = (BString*)fPreviousExpressions.ItemAtFast(i);
		status_t ret = archive->AddString("previous expression",
			item->String());
		if (ret < B_OK)
			return ret;
	}
	return B_OK;
}