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