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