xref: /haiku/src/apps/deskcalc/ExpressionTextView.cpp (revision 9f3bdf3d039430b5172c424def20ce5d9f7367d4)
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 	SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
46 	SetAlignment(B_ALIGN_RIGHT);
47 }
48 
49 
50 ExpressionTextView::~ExpressionTextView()
51 {
52 	int32 count = fPreviousExpressions.CountItems();
53 	for (int32 i = 0; i < count; i++)
54 		delete (BString*)fPreviousExpressions.ItemAtFast(i);
55 }
56 
57 
58 void
59 ExpressionTextView::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 
72 void
73 ExpressionTextView::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 
113 void
114 ExpressionTextView::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 
127 void
128 ExpressionTextView::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 
136 void
137 ExpressionTextView::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 
158 void
159 ExpressionTextView::RevertChanges()
160 {
161 	Clear();
162 }
163 
164 
165 void
166 ExpressionTextView::ApplyChanges()
167 {
168 	AddExpressionToHistory(Text());
169 	fCalcView->FlashKey("=", 1);
170 	fCalcView->Evaluate();
171 	fChangesApplied = true;
172 }
173 
174 
175 // #pragma mark -
176 
177 
178 void
179 ExpressionTextView::AddKeypadLabel(const char* label)
180 {
181 	fKeypadLabels << label;
182 }
183 
184 
185 void
186 ExpressionTextView::SetExpression(const char* expression)
187 {
188 	SetText(expression);
189 	int32 lastPos = strlen(expression);
190 	Select(lastPos, lastPos);
191 }
192 
193 
194 void
195 ExpressionTextView::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 
352 void
353 ExpressionTextView::BackSpace()
354 {
355 	const char bytes[1] = { B_BACKSPACE };
356 	KeyDown(bytes, 1);
357 
358 	fCalcView->FlashKey("BS", 2);
359 }
360 
361 
362 void
363 ExpressionTextView::Clear()
364 {
365 	SetText("");
366 
367 	fCalcView->FlashKey("C", 1);
368 }
369 
370 
371 // #pragma mark -
372 
373 
374 void
375 ExpressionTextView::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 
403 void
404 ExpressionTextView::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 
424 void
425 ExpressionTextView::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 
449 void
450 ExpressionTextView::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 
461 status_t
462 ExpressionTextView::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