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