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