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