xref: /haiku/src/apps/deskcalc/ExpressionTextView.cpp (revision fed8255311c711fdb1508d849002c5ed217e8f45)
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)
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 	// make the string shorter if it does not fit in the view
206 	float viewWidth = Frame().Width()
207 		- floorf(be_control_look->DefaultLabelSpacing() / 2);
208 	if (value.CountChars() > 3 && stringWidth > viewWidth) {
209 		// get the position of the first digit
210 		int32 firstDigit = 0;
211 		if (value[0] == '-')
212 			firstDigit++;
213 
214 		// calculate the value of the exponent
215 		int32 exponent = 0;
216 		int32 offset = value.FindFirst('.');
217 		if (offset == B_ERROR) {
218 			exponent = value.CountChars() - 1 - firstDigit;
219 			value.Insert('.', 1, firstDigit + 1);
220 		} else {
221 			if (offset == firstDigit + 1) {
222 				// if the value is 0.01 or larger then scientific notation
223 				// won't shorten the string
224 				if (value[firstDigit] != '0' || value[firstDigit + 2] != '0'
225 					|| value[firstDigit + 3] != '0') {
226 					exponent = 0;
227 				} else {
228 					// remove the period
229 					value.Remove(offset, 1);
230 
231 					// check for negative exponent value
232 					exponent = 0;
233 					while (value[firstDigit] == '0') {
234 						value.Remove(firstDigit, 1);
235 						exponent--;
236 					}
237 
238 					// add the period
239 					value.Insert('.', 1, firstDigit + 1);
240 				}
241 			} else {
242 				// if the period + 1 digit fits in the view scientific notation
243 				// won't shorten the string
244 				BString temp = value;
245 				temp.Truncate(offset + 2);
246 				stringWidth = font.StringWidth(temp);
247 				if (stringWidth < viewWidth)
248 					exponent = 0;
249 				else {
250 					// move the period
251 					value.Remove(offset, 1);
252 					value.Insert('.', 1, firstDigit + 1);
253 
254 					exponent = offset - (firstDigit + 1);
255 				}
256 			}
257 		}
258 
259 		if (exponent != 0) {
260 			value.Truncate(40);
261 				// truncate to a reasonable precision
262 				// while ensuring result will be rounded
263 			offset = value.CountChars() - 1;
264 			value << "E" << exponent;
265 				// add the exponent
266 		} else
267 			offset = value.CountChars() - 1;
268 
269 		// reduce the number of digits until the string fits or can not be
270 		// made any shorter
271 		stringWidth = font.StringWidth(value);
272 		char lastRemovedDigit = '0';
273 		while (offset > firstDigit && stringWidth > viewWidth) {
274 			if (value[offset] != '.')
275 				lastRemovedDigit = value[offset];
276 			value.Remove(offset--, 1);
277 			stringWidth = font.StringWidth(value);
278 		}
279 
280 		// no need to keep the period if no digits follow
281 		if (value[offset] == '.') {
282 			value.Remove(offset, 1);
283 			offset--;
284 		}
285 
286 		// take care of proper rounding of the result
287 		int digit = (int)lastRemovedDigit - '0'; // ascii to int
288 		if (digit >= 5) {
289 			for (; offset >= firstDigit; offset--) {
290 				if (value[offset] == '.')
291 					continue;
292 
293 				digit = (int)(value[offset]) - '0' + 1; // ascii to int + 1
294 				if (digit != 10)
295 					break;
296 
297 				value.SetByteAt(offset, '0');
298 			}
299 			if (digit == 10) {
300 				// carry over, shift the result
301 				if (value[firstDigit + 1] == '.') {
302 					value.SetByteAt(firstDigit + 1, '0');
303 					value.SetByteAt(firstDigit, '.');
304 				}
305 				value.Insert('1', 1, firstDigit);
306 
307 				// remove the exponent value and the last digit
308 				offset = value.FindFirst('E');
309 				if (offset == B_ERROR)
310 					offset = value.CountChars();
311 
312 				value.Truncate(--offset);
313 				offset--; // offset now points to the last digit
314 
315 				// increase the exponent and add it back to the string
316 				exponent++;
317 				value << 'E' << exponent;
318 			} else {
319 				// increase the current digit value with one
320 				value.SetByteAt(offset, char(digit + 48));
321 
322 				// set offset to last digit
323 				offset = value.FindFirst('E');
324 				if (offset == B_ERROR)
325 					offset = value.CountChars();
326 
327 				offset--;
328 			}
329 		}
330 
331 		// clean up decimal part if we have one
332 		if (value.FindFirst('.') != B_ERROR) {
333 			// remove trailing zeros
334 			while (value[offset] == '0')
335 				value.Remove(offset--, 1);
336 
337 			// no need to keep the period if no digits follow
338 			if (value[offset] == '.')
339 				value.Remove(offset, 1);
340 		}
341 	}
342 
343 	// set the new value
344 	SetExpression(value);
345 }
346 
347 
348 void
349 ExpressionTextView::BackSpace()
350 {
351 	const char bytes[1] = { B_BACKSPACE };
352 	KeyDown(bytes, 1);
353 
354 	fCalcView->FlashKey("BS", 2);
355 }
356 
357 
358 void
359 ExpressionTextView::Clear()
360 {
361 	SetText("");
362 
363 	fCalcView->FlashKey("C", 1);
364 }
365 
366 
367 // #pragma mark -
368 
369 
370 void
371 ExpressionTextView::AddExpressionToHistory(const char* expression)
372 {
373 	// clean out old expressions that are the same as
374 	// the one to be added
375 	int32 count = fPreviousExpressions.CountItems();
376 	for (int32 i = 0; i < count; i++) {
377 		BString* item = (BString*)fPreviousExpressions.ItemAt(i);
378 		if (*item == expression && fPreviousExpressions.RemoveItem(i)) {
379 			delete item;
380 			i--;
381 			count--;
382 		}
383 	}
384 
385 	BString* item = new (nothrow) BString(expression);
386 	if (!item)
387 		return;
388 	if (!fPreviousExpressions.AddItem(item)) {
389 		delete item;
390 		return;
391 	}
392 	while (fPreviousExpressions.CountItems() > kMaxPreviousExpressions)
393 		delete (BString*)fPreviousExpressions.RemoveItem((int32)0);
394 
395 	fHistoryPos = fPreviousExpressions.CountItems();
396 }
397 
398 
399 void
400 ExpressionTextView::PreviousExpression()
401 {
402 	int32 count = fPreviousExpressions.CountItems();
403 	if (fHistoryPos == count) {
404 		// save current expression
405 		fCurrentExpression = Text();
406 	}
407 
408 	fHistoryPos--;
409 	if (fHistoryPos < 0) {
410 		fHistoryPos = 0;
411 		return;
412 	}
413 
414 	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
415 	if (item != NULL)
416 		SetExpression(item->String());
417 }
418 
419 
420 void
421 ExpressionTextView::NextExpression()
422 {
423 	int32 count = fPreviousExpressions.CountItems();
424 
425 	fHistoryPos++;
426 	if (fHistoryPos == count) {
427 		SetExpression(fCurrentExpression.String());
428 		return;
429 	}
430 
431 	if (fHistoryPos > count) {
432 		fHistoryPos = count;
433 		return;
434 	}
435 
436 	BString* item = (BString*)fPreviousExpressions.ItemAt(fHistoryPos);
437 	if (item)
438 		SetExpression(item->String());
439 }
440 
441 
442 // #pragma mark -
443 
444 
445 void
446 ExpressionTextView::LoadSettings(const BMessage* archive)
447 {
448 	const char* oldExpression;
449 	for (int32 i = 0;
450 		archive->FindString("previous expression", i, &oldExpression) == B_OK;
451 		i++) {
452 		AddExpressionToHistory(oldExpression);
453 	}
454 }
455 
456 
457 status_t
458 ExpressionTextView::SaveSettings(BMessage* archive) const
459 {
460 	int32 count = fPreviousExpressions.CountItems();
461 	for (int32 i = 0; i < count; i++) {
462 		BString* item = (BString*)fPreviousExpressions.ItemAtFast(i);
463 		status_t ret = archive->AddString("previous expression",
464 			item->String());
465 		if (ret < B_OK)
466 			return ret;
467 	}
468 	return B_OK;
469 }
470