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