1/*
2 * Copyright 2006-2013 Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Stephan A��mus, superstippi@gmx.de
7 *		John Scipione, jscipione@gmail.com
8 */
9
10
11#include "ExpressionTextView.h"
12
13#include <new>
14#include <stdio.h>
15
16#include <Beep.h>
17#include <ControlLook.h>
18#include <Window.h>
19
20#include "CalcView.h"
21
22
23using std::nothrow;
24
25static const int32 kMaxPreviousExpressions = 20;
26
27
28ExpressionTextView::ExpressionTextView(BRect frame, CalcView* calcView)
29	:
30	InputTextView(frame, "expression text view",
31		(frame.OffsetToCopy(B_ORIGIN)).InsetByCopy(2, 2),
32		B_FOLLOW_NONE, B_WILL_DRAW),
33	fCalcView(calcView),
34	fKeypadLabels(""),
35	fPreviousExpressions(20),
36	fHistoryPos(0),
37	fCurrentExpression(""),
38	fCurrentValue(""),
39	fChangesApplied(false)
40{
41	SetStylable(false);
42	SetDoesUndo(true);
43	SetColorSpace(B_RGB32);
44	SetFontAndColor(be_bold_font, B_FONT_ALL);
45	SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
46	SetAlignment(B_ALIGN_RIGHT);
47}
48
49
50ExpressionTextView::~ExpressionTextView()
51{
52	int32 count = fPreviousExpressions.CountItems();
53	for (int32 i = 0; i < count; i++)
54		delete (BString*)fPreviousExpressions.ItemAtFast(i);
55}
56
57
58void
59ExpressionTextView::MakeFocus(bool focused)
60{
61	if (focused == IsFocus()) {
62		// stop endless loop when CalcView calls us again
63		return;
64	}
65
66	// NOTE: order of lines important!
67	InputTextView::MakeFocus(focused);
68	fCalcView->MakeFocus(focused);
69}
70
71
72void
73ExpressionTextView::KeyDown(const char* bytes, int32 numBytes)
74{
75	// Handle expression history
76	if (bytes[0] == B_UP_ARROW) {
77		PreviousExpression();
78		return;
79	}
80	if (bytes[0] == B_DOWN_ARROW) {
81		NextExpression();
82		return;
83	}
84	BString current = Text();
85
86	// Handle in InputTextView, except B_TAB
87	if (bytes[0] == '=')
88		ApplyChanges();
89	else if (bytes[0] != B_TAB)
90		InputTextView::KeyDown(bytes, numBytes);
91
92	// Pass on to CalcView if this was a label on a key
93	if (fKeypadLabels.FindFirst(bytes[0]) >= 0)
94		fCalcView->FlashKey(bytes, numBytes);
95	else if (bytes[0] == B_BACKSPACE)
96		fCalcView->FlashKey("BS", 2);
97
98	// As soon as something is typed, we are at the end of the expression
99	// history.
100	if (current != Text())
101		fHistoryPos = fPreviousExpressions.CountItems();
102
103	// If changes where not applied the value has become a new expression
104	// note that even if only the left or right arrow keys are pressed the
105	// fCurrentValue string will be cleared.
106	if (!fChangesApplied)
107		fCurrentValue.SetTo("");
108	else
109		fChangesApplied = false;
110}
111
112
113void
114ExpressionTextView::MouseDown(BPoint where)
115{
116	uint32 buttons;
117	Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons);
118	if (buttons & B_PRIMARY_MOUSE_BUTTON) {
119		InputTextView::MouseDown(where);
120		return;
121	}
122	where = ConvertToParent(where);
123	fCalcView->MouseDown(where);
124}
125
126
127void
128ExpressionTextView::GetDragParameters(BMessage* dragMessage,
129	BBitmap** bitmap, BPoint* point, BHandler** handler)
130{
131	InputTextView::GetDragParameters(dragMessage, bitmap, point, handler);
132	dragMessage->AddString("be:clip_name", "DeskCalc clipping");
133}
134
135
136void
137ExpressionTextView::SetTextRect(BRect rect)
138{
139	float hInset = floorf(be_control_look->DefaultLabelSpacing() / 2);
140	float vInset = floorf((rect.Height() - LineHeight(0)) / 2);
141	InputTextView::SetInsets(hInset, vInset, hInset, vInset);
142	InputTextView::SetTextRect(rect);
143
144	int32 count = fPreviousExpressions.CountItems();
145	if (fHistoryPos == count && fCurrentValue.CountChars() > 0) {
146		int32 start;
147		int32 finish;
148		GetSelection(&start, &finish);
149		SetValue(fCurrentValue.String());
150		Select(start, finish);
151	}
152}
153
154
155// #pragma mark -
156
157
158void
159ExpressionTextView::RevertChanges()
160{
161	Clear();
162}
163
164
165void
166ExpressionTextView::ApplyChanges()
167{
168	AddExpressionToHistory(Text());
169	fCalcView->FlashKey("=", 1);
170	fCalcView->Evaluate();
171	fChangesApplied = true;
172}
173
174
175// #pragma mark -
176
177
178void
179ExpressionTextView::AddKeypadLabel(const char* label)
180{
181	fKeypadLabels << label;
182}
183
184
185void
186ExpressionTextView::SetExpression(const char* expression)
187{
188	SetText(expression);
189	int32 lastPos = strlen(expression);
190	Select(lastPos, lastPos);
191}
192
193
194void
195ExpressionTextView::SetValue(BString value, BString decimalSeparator)
196{
197	// save the value
198	fCurrentValue = value;
199
200	// calculate the width of the string
201	BFont font;
202	uint32 mode = B_FONT_ALL;
203	GetFontAndColor(&font, &mode);
204	float stringWidth = font.StringWidth(value);
205
206	uint decimalSeparatorWidth = decimalSeparator.CountChars();
207
208	// make the string shorter if it does not fit in the view
209	float viewWidth = Frame().Width()
210		- floorf(be_control_look->DefaultLabelSpacing() / 2);
211	if (value.CountChars() > 3 && stringWidth > viewWidth) {
212		// get the position of the first digit
213		int32 firstDigit = 0;
214		if (value[0] == '-')
215			firstDigit++;
216
217		// calculate the value of the exponent
218		int32 exponent = 0;
219		int32 offset = value.FindFirst(decimalSeparator);
220		if (offset == B_ERROR) {
221			exponent = value.CountChars() - decimalSeparatorWidth - firstDigit;
222			value.InsertChars(decimalSeparator, firstDigit + 1);
223		} else {
224			if (offset == firstDigit + 1) {
225				// if the value is 0.01 or larger then scientific notation
226				// won't shorten the string
227				if (value[firstDigit] != '0' || value[firstDigit + 2] != '0'
228					|| value[firstDigit + 3] != '0') {
229					exponent = 0;
230				} else {
231					// remove the period
232					value.Remove(offset, decimalSeparatorWidth);
233
234					// check for negative exponent value
235					exponent = 0;
236					while (value[firstDigit] == '0') {
237						value.Remove(firstDigit, 1);
238						exponent--;
239					}
240
241					// add the period
242					value.InsertChars(decimalSeparator, firstDigit + 1);
243				}
244			} else {
245				// if the period + 1 digit fits in the view scientific notation
246				// won't shorten the string
247				BString temp = value;
248				temp.Truncate(offset + 2);
249				stringWidth = font.StringWidth(temp);
250				if (stringWidth < viewWidth)
251					exponent = 0;
252				else {
253					// move the period
254					value.Remove(offset, decimalSeparatorWidth);
255					value.InsertChars(decimalSeparator, firstDigit + 1);
256
257					exponent = offset - (firstDigit + 1);
258				}
259			}
260		}
261
262		if (exponent != 0) {
263			value.Truncate(40);
264				// truncate to a reasonable precision
265				// while ensuring result will be rounded
266			offset = value.CountChars() - 1;
267			value << "E" << exponent;
268				// add the exponent
269		} else
270			offset = value.CountChars() - 1;
271
272		// reduce the number of digits until the string fits or can not be
273		// made any shorter
274		stringWidth = font.StringWidth(value);
275		char lastRemovedDigit = '0';
276		while (offset > firstDigit && stringWidth > viewWidth) {
277			if (value.CharAt(offset) != decimalSeparator)
278				lastRemovedDigit = value[offset];
279			value.Remove(offset--, 1);
280			stringWidth = font.StringWidth(value);
281		}
282
283		// no need to keep the period if no digits follow
284		if (value.CharAt(offset) == decimalSeparator) {
285			value.Remove(offset, decimalSeparatorWidth);
286			offset--;
287		}
288
289		// take care of proper rounding of the result
290		int digit = (int)lastRemovedDigit - '0'; // ascii to int
291		if (digit >= 5) {
292			for (; offset >= firstDigit; offset--) {
293				if (value.CharAt(offset) == decimalSeparator)
294					continue;
295
296				digit = (int)(value[offset]) - '0' + 1; // ascii to int + 1
297				if (digit != 10)
298					break;
299
300				value.SetByteAt(offset, '0');
301			}
302			if (digit == 10) {
303				// carry over, shift the result
304				if (value.CharAt(firstDigit + 1) == decimalSeparator) {
305					value.SetByteAt(firstDigit + decimalSeparatorWidth, '0');
306					value.RemoveChars(firstDigit, decimalSeparatorWidth);
307					value.InsertChars(decimalSeparator, firstDigit);
308				}
309				value.Insert('1', 1, firstDigit);
310
311				// remove the exponent value and the last digit
312				offset = value.FindFirst('E');
313				if (offset == B_ERROR)
314					offset = value.CountChars();
315
316				value.Truncate(--offset);
317				offset--; // offset now points to the last digit
318
319				// increase the exponent and add it back to the string
320				exponent++;
321				value << 'E' << exponent;
322			} else {
323				// increase the current digit value with one
324				value.SetByteAt(offset, char(digit + 48));
325
326				// set offset to last digit
327				offset = value.FindFirst('E');
328				if (offset == B_ERROR)
329					offset = value.CountChars();
330
331				offset--;
332			}
333		}
334
335		// clean up decimal part if we have one
336		if (value.FindFirst(decimalSeparator) != B_ERROR) {
337			// remove trailing zeros
338			while (value[offset] == '0')
339				value.Remove(offset--, 1);
340
341			// no need to keep the period if no digits follow
342			if (value.CharAt(offset) == decimalSeparator)
343				value.Remove(offset, decimalSeparatorWidth);
344		}
345	}
346
347	// set the new value
348	SetExpression(value);
349}
350
351
352void
353ExpressionTextView::BackSpace()
354{
355	const char bytes[1] = { B_BACKSPACE };
356	KeyDown(bytes, 1);
357
358	fCalcView->FlashKey("BS", 2);
359}
360
361
362void
363ExpressionTextView::Clear()
364{
365	SetText("");
366
367	fCalcView->FlashKey("C", 1);
368}
369
370
371// #pragma mark -
372
373
374void
375ExpressionTextView::AddExpressionToHistory(const char* expression)
376{
377	// clean out old expressions that are the same as
378	// the one to be added
379	int32 count = fPreviousExpressions.CountItems();
380	for (int32 i = 0; i < count; i++) {
381		BString* item = (BString*)fPreviousExpressions.ItemAt(i);
382		if (*item == expression && fPreviousExpressions.RemoveItem(i)) {
383			delete item;
384			i--;
385			count--;
386		}
387	}
388
389	BString* item = new (nothrow) BString(expression);
390	if (!item)
391		return;
392	if (!fPreviousExpressions.AddItem(item)) {
393		delete item;
394		return;
395	}
396	while (fPreviousExpressions.CountItems() > kMaxPreviousExpressions)
397		delete (BString*)fPreviousExpressions.RemoveItem((int32)0);
398
399	fHistoryPos = fPreviousExpressions.CountItems();
400}
401
402
403void
404ExpressionTextView::PreviousExpression()
405{
406	int32 count = fPreviousExpressions.CountItems();
407	if (fHistoryPos == count) {
408		// save current expression
409		fCurrentExpression = Text();
410	}
411
412	fHistoryPos--;
413	if (fHistoryPos < 0) {
414		fHistoryPos = 0;
415		return;
416	}
417
418	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
419	if (item != NULL)
420		SetExpression(item->String());
421}
422
423
424void
425ExpressionTextView::NextExpression()
426{
427	int32 count = fPreviousExpressions.CountItems();
428
429	fHistoryPos++;
430	if (fHistoryPos == count) {
431		SetExpression(fCurrentExpression.String());
432		return;
433	}
434
435	if (fHistoryPos > count) {
436		fHistoryPos = count;
437		return;
438	}
439
440	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
441	if (item)
442		SetExpression(item->String());
443}
444
445
446// #pragma mark -
447
448
449void
450ExpressionTextView::LoadSettings(const BMessage* archive)
451{
452	const char* oldExpression;
453	for (int32 i = 0;
454		archive->FindString("previous expression", i, &oldExpression) == B_OK;
455		i++) {
456		AddExpressionToHistory(oldExpression);
457	}
458}
459
460
461status_t
462ExpressionTextView::SaveSettings(BMessage* archive) const
463{
464	int32 count = fPreviousExpressions.CountItems();
465	for (int32 i = 0; i < count; i++) {
466		BString* item = (BString*)fPreviousExpressions.ItemAtFast(i);
467		status_t ret = archive->AddString("previous expression",
468			item->String());
469		if (ret < B_OK)
470			return ret;
471	}
472	return B_OK;
473}
474