ComputerscareBlank.cpp (37527B)
1 #include "Computerscare.hpp" 2 #include "ComputerscareResizableHandle.hpp" 3 #include "animatedGif.hpp" 4 #include "CustomBlankFunctions.hpp" 5 6 #include <osdialog.h> 7 #include <iostream> 8 #include <fstream> 9 #include <sstream> 10 #include <thread> 11 #include <dirent.h> 12 #include <algorithm> 13 #include <random> 14 #include <settings.hpp> 15 #include <cctype> 16 #include <algorithm> 17 18 struct ComputerscareBlank : ComputerscareMenuParamModule { 19 bool loading = true; 20 bool loadedJSON = false; 21 bool jsonFlag = false; 22 23 bool ready = false; 24 std::string path; 25 std::string parentDirectory; 26 27 std::vector<std::string> paths; 28 std::vector<std::string> catalog; 29 int fileIndexInCatalog; 30 unsigned int numFilesInCatalog = 0; 31 32 float width = 120; 33 float height = 380; 34 int rotation = 0; 35 bool invertY = true; 36 float zoomX = 1.f; 37 float zoomY = 1.f; 38 float xOffset = 0.f; 39 float yOffset = 0.f; 40 int imageFitEnum = 0; 41 int currentFrame = 0; 42 int mappedFrame = 0; 43 int numFrames = 0; 44 int sampleCounter = 0; 45 float frameDelay = .5; 46 std::vector<float> frameDelays; 47 std::vector<int> frameMapForScan; 48 std::vector<int> shuffledFrames; 49 std::vector<float> gifDurationsForPingPong; 50 std::vector<int> framesForward; 51 std::vector<int> framesReverse; 52 std::vector<int> framesPingpong; 53 54 std::vector<std::vector<int>> frameScripts; 55 56 float totalGifDuration = 0.f; 57 58 int samplesDelay = 10000; 59 int speed = 100000; 60 int imageStatus = 0; 61 bool scrubbing = false; 62 int scrubFrame = 0; 63 64 int pingPongDirection = 1; 65 66 float speedFactor = 1.f; 67 68 float zeroOffset = 0.f; 69 70 bool tick = false; 71 float lastShuffle = 2.f; 72 73 float lastZoom = -100; 74 int zoomCheckInterval = 5000; 75 int zoomCheckCounter = 0; 76 bool pauseAnimation = true; 77 78 /* 79 uninitialized: 0 80 gif: 1 81 not gif: 2 82 error:3 83 */ 84 85 bool expanderConnected = false; 86 87 int clockMode = CLOCK_MODE_SYNC; 88 bool clockConnected = false; 89 bool resetConnected = false; 90 bool nextFileInputConnected = false; 91 92 std::vector<std::string> animationModeDescriptions; 93 std::vector<std::string> endBehaviorDescriptions; 94 std::vector<std::string> nextFileDescriptions; 95 96 97 dsp::SchmittTrigger clockTrigger; 98 dsp::SchmittTrigger resetTrigger; 99 dsp::SchmittTrigger resetButtonTrigger; 100 101 dsp::SchmittTrigger nextFileTrigger; 102 dsp::SchmittTrigger nextFileButtonTrigger; 103 104 dsp::Timer syncTimer; 105 dsp::Timer slideshowTimer; 106 107 dsp::PulseGenerator resetTriggerPulse; 108 109 110 ComputerscareSVGPanel* panelRef; 111 112 float leftMessages[2][11] = {}; 113 114 enum ClockModes { 115 CLOCK_MODE_SYNC, 116 CLOCK_MODE_SCAN, 117 CLOCK_MODE_FRAME 118 }; 119 120 enum AnimationModes { 121 ANIMATION_FORWARD, 122 ANIMATION_REVERSE, 123 ANIMATION_PINGPONG, 124 ANIMATION_SHUFFLE, 125 ANIMATION_RANDOM 126 }; 127 enum ParamIds { 128 ANIMATION_SPEED, 129 ANIMATION_ENABLED, 130 CONSTANT_FRAME_DELAY, 131 ANIMATION_MODE, 132 END_BEHAVIOR, 133 SHUFFLE_SEED, 134 NEXT_FILE_BEHAVIOR, 135 SLIDESHOW_ACTIVE, 136 SLIDESHOW_TIME, 137 LIGHT_WIDGET_MODE, 138 NUM_PARAMS 139 }; 140 enum InputIds { 141 NUM_INPUTS 142 }; 143 enum OutputIds { 144 NUM_OUTPUTS 145 }; 146 enum LightIds { 147 NUM_LIGHTS 148 }; 149 150 151 ComputerscareBlank() { 152 animationModeDescriptions.push_back("Forward"); 153 animationModeDescriptions.push_back("Reverse"); 154 animationModeDescriptions.push_back("Ping Pong"); 155 animationModeDescriptions.push_back("Random Shuffled"); 156 animationModeDescriptions.push_back("Full Random"); 157 158 endBehaviorDescriptions.push_back("Repeat"); 159 endBehaviorDescriptions.push_back("Stop"); 160 endBehaviorDescriptions.push_back("Select Random"); 161 endBehaviorDescriptions.push_back("Load Next"); 162 endBehaviorDescriptions.push_back("Load Previous"); 163 164 nextFileDescriptions.push_back("Load Next (Alphabetical) File in Directory"); 165 nextFileDescriptions.push_back("Load Previous (Alphabetical) File in Directory"); 166 nextFileDescriptions.push_back("Load Random File from Directory"); 167 168 config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 169 170 configMenuParam(ANIMATION_SPEED, -1.f, 1.f, 0.f, "Animation Speed", 2, "x", 20.f); 171 configParam(ANIMATION_ENABLED, 0.f, 1.f, 1.f, "Animation Enabled"); 172 configParam(CONSTANT_FRAME_DELAY, 0.f, 1.f, 0.f, "Constant Frame Delay"); 173 configMenuParam(ANIMATION_MODE, 0.f, "Animation Mode", animationModeDescriptions); 174 configMenuParam(NEXT_FILE_BEHAVIOR, 0.f, "Next File Trigger / Button Behavior", nextFileDescriptions); 175 configMenuParam(SHUFFLE_SEED, 0.f, 1.f, 0.5f, "Shuffle Seed", 2); 176 177 configParam(SLIDESHOW_ACTIVE, 0.f, 1.f, 0.f, "Slideshow Active"); 178 configMenuParam(SLIDESHOW_TIME, 0.f, 1.f, 0.200948f, "Slideshow Time", 2, " s", 400.f, 3.f); 179 configParam(LIGHT_WIDGET_MODE, 0.f, 1.f, 0.f, "Keep image fully opaque when dimming room lights"); 180 181 paths.push_back("empty"); 182 183 leftExpander.producerMessage = leftMessages[0]; 184 leftExpander.consumerMessage = leftMessages[1]; 185 186 } 187 void process(const ProcessArgs &args) override { 188 if (imageStatus == 1) { 189 sampleCounter++; 190 zoomCheckCounter++; 191 if (zoomCheckCounter > zoomCheckInterval) { 192 float zoom = APP->scene->rackScroll->getZoom(); 193 if (zoom != lastZoom) { 194 pauseAnimation = true; 195 } 196 else { 197 pauseAnimation = false; 198 } 199 lastZoom = zoom; 200 zoomCheckCounter = 0; 201 } 202 } 203 204 if (params[SLIDESHOW_ACTIVE].getValue()) { 205 //float dTime = exp(5 * params[SLIDESHOW_TIME].getValue()); 206 float dTime = 3 * std::pow(400.f , params[SLIDESHOW_TIME].getValue()); 207 if (slideshowTimer.process(args.sampleTime) > dTime) { 208 checkAndPerformEndAction(true); 209 slideshowTimer.reset(); 210 } 211 } 212 213 samplesDelay = frameDelay * args.sampleRate; 214 215 bool shouldAdvanceAnimation = false; 216 bool clockTriggered = false; 217 218 if (ready && leftExpander.module && leftExpander.module->model == modelComputerscareBlankExpander) { 219 expanderConnected = true; 220 // me 221 float *messageToSendToExpander = (float*) leftExpander.module->rightExpander.producerMessage; 222 223 float *messageFromExpander = (float*) leftExpander.consumerMessage; 224 225 clockMode = messageFromExpander[0]; 226 clockConnected = messageFromExpander[1]; 227 resetConnected = messageFromExpander[3]; 228 nextFileInputConnected = messageFromExpander[5]; 229 230 zeroOffset = messageFromExpander[7]; 231 232 scrubbing = messageFromExpander[8]; 233 234 updateScrubFrame(); 235 236 bool resetTriggered = false; 237 bool resetTimerHigh = false; 238 239 if (resetConnected) { 240 if (resetTrigger.process(messageFromExpander[4])) { 241 resetTriggered = true; 242 243 resetTriggerPulse.trigger(1e-3f); 244 } 245 } 246 247 resetTimerHigh = resetTriggerPulse.process(args.sampleTime); 248 249 if (clockConnected) { 250 clockTriggered = clockTrigger.process(messageFromExpander[2]); 251 if (clockMode == CLOCK_MODE_SYNC) { 252 //sync 253 float currentSyncTime = syncTimer.process(args.sampleTime); 254 if (clockTriggered) { 255 syncTimer.reset(); 256 setSyncTime(currentSyncTime); 257 if (params[ANIMATION_ENABLED].getValue()) { 258 goToFrame(0); 259 } 260 } 261 } 262 263 else if (clockMode == CLOCK_MODE_SCAN) { 264 //scan 265 float scanPosition = messageFromExpander[2]; 266 scanToPosition(scanPosition); 267 } 268 else if (clockMode == CLOCK_MODE_FRAME) { 269 //frame advance 270 //should be ignored if being reset 271 shouldAdvanceAnimation = clockTriggered && !resetTimerHigh; 272 } 273 } 274 275 if (nextFileInputConnected) { 276 if (nextFileTrigger.process(messageFromExpander[6])) { 277 checkAndPerformEndAction(true); 278 } 279 } 280 281 if (nextFileButtonTrigger.process(messageFromExpander[10])) { 282 checkAndPerformEndAction(true); 283 } 284 285 if (resetTriggered) { 286 goToFrame(0); 287 } 288 if (resetButtonTrigger.process(messageFromExpander[9])) { 289 goToFrame(0); 290 } 291 292 messageToSendToExpander[0] = float (currentFrame); 293 messageToSendToExpander[1] = float (numFrames); 294 messageToSendToExpander[2] = float (mappedFrame); 295 messageToSendToExpander[3] = float (scrubFrame); 296 messageToSendToExpander[4] = float (tick); 297 298 // Flip messages at the end of the timestep 299 leftExpander.module->rightExpander.messageFlipRequested = true; 300 } 301 else { 302 expanderConnected = false; 303 } 304 305 if (expanderConnected && clockConnected && (clockMode == CLOCK_MODE_FRAME)) { 306 //no-op for frame mode for some reason? 307 } 308 else { 309 if (sampleCounter > samplesDelay) { 310 sampleCounter = 0; 311 shouldAdvanceAnimation = true; 312 } 313 } 314 if (numFrames > 1) { 315 if (params[ANIMATION_ENABLED].getValue() && shouldAdvanceAnimation) { 316 tickAnimation(); 317 //checkAndPerformEndAction(); 318 } 319 } 320 else { 321 if ((clockTriggered && (clockConnected && clockMode == CLOCK_MODE_SYNC)) || numFrames > 1) { 322 if (currentFrame == 0) { 323 //checkAndPerformEndAction(); 324 } 325 } 326 } 327 } 328 329 bool getLightWidgetMode() { 330 return params[LIGHT_WIDGET_MODE].getValue(); 331 } 332 void updateScrubFrame() { 333 if (ready) { 334 scrubFrame = mapBlankFrameOffset(zeroOffset, numFrames); 335 } 336 } 337 338 void onReset() override { 339 zoomX = 1; 340 zoomY = 1; 341 xOffset = 0; 342 yOffset = 0; 343 } 344 void loadImageDialog(int index = 0) { 345 std::string dir = this->paths[index].empty() ? asset::user("../") : asset::user(this->paths[index]); 346 char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); 347 if (!pathC) { 348 return; 349 } 350 351 std::string path = pathC; 352 std::free(pathC); 353 354 setPath(path); 355 jsonFlag = false; 356 } 357 void setContainingDirectory(int index = 0) { 358 std::string dir = system::getDirectory(asset::user(paths[index])); 359 std::string currentImageFullpath; 360 parentDirectory = dir; 361 362 int imageIndex = 0;; 363 364 struct dirent* dirp = NULL; 365 DIR* rep = NULL; 366 rep = opendir(dir.c_str()); 367 catalog.clear(); 368 369 if (rep) { 370 while ((dirp = readdir(rep)) != NULL) { 371 std::string name = dirp->d_name; 372 373 std::size_t found = name.find(".gif", name.length() - 5); 374 if (found == std::string::npos) found = name.find(".GIF", name.length() - 5); 375 if (found == std::string::npos) found = name.find(".png", name.length() - 5); 376 if (found == std::string::npos) found = name.find(".PNG", name.length() - 5); 377 if (found == std::string::npos) found = name.find(".jpg", name.length() - 5); 378 if (found == std::string::npos) found = name.find(".JPG", name.length() - 5); 379 if (found == std::string::npos) found = name.find(".jpeg", name.length() - 5); 380 if (found == std::string::npos) found = name.find(".JPEG", name.length() - 5); 381 if (found == std::string::npos) found = name.find(".bmp", name.length() - 5); 382 if (found == std::string::npos) found = name.find(".BMP", name.length() - 5); 383 if (found != std::string::npos) { 384 currentImageFullpath = parentDirectory + "/" + name; 385 catalog.push_back(currentImageFullpath); 386 if (currentImageFullpath == paths[index]) { 387 fileIndexInCatalog = imageIndex; 388 } 389 imageIndex++; 390 } 391 } 392 } 393 numFilesInCatalog = catalog.size(); 394 } 395 396 void loadRandomGif() { 397 fileIndexInCatalog = floor(random::uniform() * numFilesInCatalog); 398 loadNewFileByIndex(); 399 } 400 401 void loadNewFileByIndex() { 402 if (numFilesInCatalog > 0) { 403 setPath(catalog[fileIndexInCatalog]); 404 } 405 } 406 void nextFileInCatalog() { 407 if (numFilesInCatalog > 0) { 408 fileIndexInCatalog++; 409 fileIndexInCatalog %= numFilesInCatalog; 410 loadNewFileByIndex(); 411 } 412 } 413 void prevFileInCatalog() { 414 if (numFilesInCatalog > 0) { 415 fileIndexInCatalog--; 416 fileIndexInCatalog += numFilesInCatalog; 417 fileIndexInCatalog %= numFilesInCatalog; 418 loadNewFileByIndex(); 419 } 420 } 421 void goToFileInCatelog(int index) { 422 fileIndexInCatalog = index; 423 fileIndexInCatalog %= numFilesInCatalog; 424 loadNewFileByIndex(); 425 } 426 427 void setPath(std::string path, int index = 0) { 428 numFrames = 0; 429 paths[index] = path; 430 currentFrame = 0; 431 } 432 void setFrameCount(int frameCount) { 433 numFrames = frameCount; 434 } 435 void setImageStatus(int status) { 436 imageStatus = status; 437 } 438 void setSyncTime(float syncDuration) { 439 bool constantFrameDelay = params[CONSTANT_FRAME_DELAY].getValue() == 1; 440 if (params[ANIMATION_MODE].getValue() == ANIMATION_PINGPONG) { 441 if (numFrames > 1) { 442 float totalDurationForCurrentZeroOffset = gifDurationsForPingPong[mapBlankFrameOffset(zeroOffset, numFrames)]; 443 if (constantFrameDelay) { 444 speedFactor = (2 * numFrames - 2) * defaultFrameDelayCentiseconds / syncDuration / 100; 445 } 446 else { 447 speedFactor = totalDurationForCurrentZeroOffset / syncDuration; 448 } 449 } 450 } 451 else { //all other modes 452 if (constantFrameDelay) { 453 speedFactor = numFrames * defaultFrameDelayCentiseconds / syncDuration / 100; 454 } 455 else { 456 speedFactor = totalGifDuration / syncDuration; 457 } 458 } 459 } 460 void setFrameDelay(float frameDelaySeconds) { 461 float speedKnob = std::pow(20.f, params[ANIMATION_SPEED].getValue()); 462 float appliedSpeedDivisor = 1; 463 float base = frameDelaySeconds; 464 465 if (expanderConnected && clockConnected && (clockMode == CLOCK_MODE_SYNC)) { 466 appliedSpeedDivisor = speedFactor; 467 } 468 else { 469 appliedSpeedDivisor = speedKnob; 470 } 471 472 473 if (params[CONSTANT_FRAME_DELAY].getValue()) { 474 frameDelay = defaultFrameDelayCentiseconds / appliedSpeedDivisor / 100; 475 } 476 else { 477 frameDelay = base / appliedSpeedDivisor; 478 } 479 480 } 481 void setFrameDelays(std::vector<float> frameDelaysSeconds) { 482 frameDelays = frameDelaysSeconds; 483 setFrameMap(); 484 setFrameShuffle(); 485 setRegularFrameOrders(); 486 487 frameScripts.resize(0); 488 frameScripts.push_back(framesForward); 489 frameScripts.push_back(framesReverse); 490 frameScripts.push_back(framesPingpong); 491 frameScripts.push_back(shuffledFrames); 492 frameScripts.push_back(framesForward); 493 494 } 495 void setReady(bool v) { 496 ready = v; 497 } 498 void setFrameMap() { 499 frameMapForScan.resize(0); 500 501 // tokenStack.insert(tokenStack.end(), terminalStack.begin(), terminalStack.end()); 502 503 for (unsigned int i = 0; i < frameDelays.size(); i++) { 504 int currentCentiseconds = (int) (frameDelays[i] * 100); 505 for (int j = 0; j < currentCentiseconds; j++) { 506 frameMapForScan.push_back(i); 507 } 508 } 509 } 510 void setFrameShuffle() { 511 shuffledFrames.resize(0); 512 for (int i = 0; i < numFrames; i++) { 513 shuffledFrames.push_back(i); 514 } 515 unsigned seed = (unsigned) (floor(params[SHUFFLE_SEED].getValue() * 999101)); 516 517 std::shuffle(std::begin(shuffledFrames), std::end(shuffledFrames), std::default_random_engine(seed)); 518 519 } 520 void setRegularFrameOrders() { 521 framesForward.resize(0); 522 framesReverse.resize(0); 523 framesPingpong.resize(0); 524 for (int i = 0; i < numFrames; i++) { 525 framesForward.push_back(i); 526 framesReverse.push_back(numFrames - i - 1); 527 framesPingpong.push_back(i); 528 } 529 framesPingpong.insert( framesPingpong.end(), framesReverse.begin() + 1, framesReverse.end() - 1 ); 530 531 532 } 533 void setTotalGifDuration(float totalDuration) { 534 totalGifDuration = totalDuration; 535 } 536 void setTotalGifDurationIfInPingPongMode(std::vector<float> durs) { 537 /* the gif has a different total pingpong duration depending on where the reset frame is 538 eg a gif with frame durations: 539 1) 100 540 2) 5 541 3) 5 542 4) 5 543 544 if ponging around frame 1 has duration: 100(1)+ 5(2) + 5(3) + 5(4) + 5(3) + 5(2) = 125 545 546 but if ponging around frame 3: 5(3) + 5(4) + 100(1) + 5(2) + 100(1) + 5(4) = 240 547 548 */ 549 gifDurationsForPingPong = durs; 550 551 } 552 std::string getPath() { 553 //return numFrames > 0 ? paths[currentFrame] : ""; 554 return paths[0]; 555 } 556 557 /* 558 enum AnimationModes { 559 ANIMATION_FORWARD, 560 ANIMATION_REVERSE, 561 ANIMATION_PINGPONG, 562 ANIMATION_SHUFFLE, 563 ANIMATION_RANDOM 564 }; 565 */ 566 void tickAnimation() { 567 if (numFrames > 1) { 568 int animationMode = params[ANIMATION_MODE].getValue(); 569 570 bool onFinalFrame = currentFrame == frameScripts[animationMode].size() - 1; 571 572 if (animationMode == ANIMATION_RANDOM) { 573 onFinalFrame = false; 574 } 575 576 if (clockConnected && clockMode == CLOCK_MODE_SYNC && onFinalFrame) { 577 //hold up animation back to 1st frame when controlled. Wait for clock signal 578 } 579 else { 580 /*if (animationMode == ANIMATION_FORWARD || animationMode == ANIMATION_SHUFFLE) { 581 nextFrame(); 582 } else if (animationMode == ANIMATION_REVERSE) { 583 prevFrame(); 584 } 585 else if (animationMode == ANIMATION_PINGPONG) { 586 if (pingPongDirection == 1) { 587 nextFrame(); 588 } else { 589 prevFrame(); 590 } 591 }*/nextFrame(); 592 } 593 if (animationMode == ANIMATION_RANDOM ) { 594 goToRandomFrame(); 595 } 596 tick = !tick; 597 if (animationMode == ANIMATION_PINGPONG) { 598 if (pingPongDirection == 1) { 599 if (currentFrame == numFrames - 1) { 600 pingPongDirection = -1; 601 } 602 } 603 else { 604 if (currentFrame == 0) { 605 pingPongDirection = 1; 606 } 607 } 608 } 609 610 float shuf = params[SHUFFLE_SEED].getValue(); 611 if (shuf != lastShuffle) { 612 setFrameShuffle(); 613 } 614 lastShuffle = shuf; 615 } 616 } 617 void checkAndPerformEndAction(bool forceEndAction = false) { 618 if (currentFrame == 0 || forceEndAction) { 619 int eb = params[NEXT_FILE_BEHAVIOR].getValue(); 620 621 if (eb == 0) { 622 nextFileInCatalog(); 623 } 624 else if (eb == 1) { 625 prevFileInCatalog(); 626 } 627 else if (eb == 2 ) { 628 loadRandomGif(); 629 } 630 } 631 } 632 void setCurrentFrameDelayFromTable() { 633 if (ready) { 634 setFrameDelay(frameDelays[mappedFrame]); 635 } 636 } 637 void nextFrame() { 638 goToFrame(currentFrame + 1); 639 640 } 641 void prevFrame() { 642 goToFrame(currentFrame - 1); 643 } 644 void goToFrame(int frameNum) { 645 if (numFrames && ready && frameNum != currentFrame) { 646 647 int animationMode = params[ANIMATION_MODE].getValue(); 648 649 sampleCounter = 0; 650 651 unsigned int numScriptFrames = frameScripts[animationMode].size(); 652 653 currentFrame = frameNum + numScriptFrames * 10; 654 currentFrame %= numScriptFrames; 655 mappedFrame = currentFrame; 656 657 mappedFrame = frameScripts[animationMode][mappedFrame]; 658 mappedFrame += mapBlankFrameOffset(zeroOffset, numFrames) + 10 * numFrames; 659 mappedFrame %= numFrames; 660 661 662 setCurrentFrameDelayFromTable(); 663 } 664 } 665 void goToRandomFrame() { 666 int randFrame = (int) std::floor(random::uniform() * numFrames); 667 goToFrame(randFrame); 668 } 669 void scanToPosition(float scanVoltage) { 670 /* 0v = frame 0 671 10.0v = frame n-1 672 673 */ 674 if (ready && numFrames > 1) { 675 int frameNum; 676 float vu = (scanVoltage) / 10.01f; 677 if (params[CONSTANT_FRAME_DELAY].getValue()) { 678 frameNum = floor((vu) * numFrames); 679 } 680 else { 681 //frameMapForScan 682 frameNum = frameMapForScan[floor(vu * frameMapForScan.size())]; 683 } 684 goToFrame(frameNum); 685 } 686 } 687 void toggleAnimationEnabled() { 688 float current = params[ANIMATION_ENABLED].getValue(); 689 if (current == 1.0) { 690 params[ANIMATION_ENABLED].setValue(0); 691 } 692 else { 693 params[ANIMATION_ENABLED].setValue(1); 694 } 695 } 696 json_t *dataToJson() override { 697 json_t *rootJ = json_object(); 698 if (paths.size() > 0) { 699 json_object_set_new(rootJ, "path", json_string(paths[0].c_str())); 700 } 701 702 json_object_set_new(rootJ, "width", json_real(width)); 703 json_object_set_new(rootJ, "imageFitEnum", json_integer(imageFitEnum)); 704 json_object_set_new(rootJ, "invertY", json_boolean(invertY)); 705 json_object_set_new(rootJ, "zoomX", json_real(zoomX)); 706 json_object_set_new(rootJ, "zoomY", json_real(zoomY)); 707 json_object_set_new(rootJ, "xOffset", json_real(xOffset)); 708 json_object_set_new(rootJ, "yOffset", json_real(yOffset)); 709 json_object_set_new(rootJ, "rotation", json_integer(rotation)); 710 return rootJ; 711 } 712 713 void dataFromJson(json_t *rootJ) override { 714 715 716 717 json_t *pathJ = json_object_get(rootJ, "path"); 718 if (pathJ) { 719 setPath(json_string_value(pathJ)); 720 } 721 722 json_t *widthJ = json_object_get(rootJ, "width"); 723 if (widthJ) 724 width = json_number_value(widthJ); 725 json_t *imageFitEnumJ = json_object_get(rootJ, "imageFitEnum"); 726 if (imageFitEnumJ) { imageFitEnum = json_integer_value(imageFitEnumJ); } 727 728 json_t *invertYJ = json_object_get(rootJ, "invertY"); 729 if (invertYJ) { invertY = json_is_true(invertYJ); } 730 json_t *zoomXJ = json_object_get(rootJ, "zoomX"); 731 if (zoomXJ) { 732 zoomX = json_number_value(zoomXJ); 733 } 734 json_t *zoomYJ = json_object_get(rootJ, "zoomY"); 735 if (zoomYJ) { 736 zoomY = json_number_value(zoomYJ); 737 } 738 json_t *xOffsetJ = json_object_get(rootJ, "xOffset"); 739 if (xOffsetJ) { 740 xOffset = json_number_value(xOffsetJ); 741 } 742 json_t *yOffsetJ = json_object_get(rootJ, "yOffset"); 743 if (yOffsetJ) { 744 yOffset = json_number_value(yOffsetJ); 745 } 746 json_t *rotationJ = json_object_get(rootJ, "rotation"); 747 if (rotationJ) { rotation = json_integer_value(rotationJ); } 748 this->loading = false; 749 } 750 751 }; 752 struct LoadImageItem : MenuItem { 753 ComputerscareBlank* blankModule; 754 void onAction(const event::Action& e) override { 755 blankModule->loadImageDialog(); 756 } 757 }; 758 struct ImageFitModeItem : MenuItem { 759 ComputerscareBlank *blank; 760 int imageFitEnum; 761 void onAction(const event::Action &e) override { 762 blank->imageFitEnum = imageFitEnum; 763 } 764 void step() override { 765 rightText = CHECKMARK(blank->imageFitEnum == imageFitEnum); 766 MenuItem::step(); 767 } 768 }; 769 struct InvertYMenuItem: MenuItem { 770 ComputerscareBlank *blank; 771 InvertYMenuItem() { 772 773 } 774 void onAction(const event::Action &e) override { 775 blank->invertY = !blank->invertY; 776 } 777 void step() override { 778 rightText = blank->invertY ? "✔" : ""; 779 MenuItem::step(); 780 } 781 }; 782 struct ssmi : MenuItem 783 { 784 int mySetVal = 1; 785 ParamQuantity *myParamQuantity; 786 ssmi(int i, ParamQuantity* pq) 787 { 788 mySetVal = i; 789 myParamQuantity = pq; 790 } 791 792 void onAction(const event::Action &e) override 793 { 794 myParamQuantity->setValue(mySetVal); 795 //pouter->setAll(mySetVal); 796 } 797 void step() override { 798 rightText = myParamQuantity->getValue() == mySetVal ? "✔" : ""; 799 MenuItem::step(); 800 } 801 }; 802 struct ParamSelectMenu : MenuItem { 803 ParamQuantity* param; 804 std::vector<std::string> options; 805 806 Menu *createChildMenu() override { 807 Menu *menu = new Menu; 808 for (unsigned int i = 0; i < options.size(); i++) { 809 ssmi *menuItem = new ssmi(i, param); 810 menuItem->text = options[i]; 811 //menuItem->pouter = pouter; 812 menu->addChild(menuItem); 813 } 814 return menu; 815 } 816 void step() override { 817 rightText = "(" + options[param->getValue()] + ") " + RIGHT_ARROW; 818 MenuItem::step(); 819 } 820 }; 821 struct KeyboardControlChildMenu : MenuItem { 822 ComputerscareBlank *blank; 823 824 Menu *createChildMenu() override { 825 Menu *menu = new Menu; 826 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "A,S,D,F: Translate image position")); 827 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Z,X: Zoom in/out")); 828 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "J,L: Previous / next frame")); 829 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "K: Go to first frame")); 830 831 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "I: Go to random frame")); 832 833 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "[ (left square bracket): Load previous image from same directory")); 834 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "] (right square bracket): Load next image from same directory")); 835 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "O: Load random image from same directory")); 836 837 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "P: Toggle animation on/off")); 838 839 840 InvertYMenuItem *invertYMenuItem = new InvertYMenuItem(); 841 invertYMenuItem->text = "Invert Y-Axis"; 842 invertYMenuItem->blank = blank; 843 menu->addChild(invertYMenuItem); 844 845 return menu; 846 } 847 848 }; 849 850 template <class TBase> 851 struct tPNGDisplay : TBase { 852 ComputerscareBlank *blankModule; 853 854 int imgWidth, imgHeight; 855 float imgRatio, widgetRatio; 856 int lastEnum = -1; 857 std::string path = "empty"; 858 int img = 0; 859 int currentFrame = -1; 860 bool missingOrBroken = false; 861 AnimatedGifBuddy gifBuddy; 862 863 bool lightWidgetMode = false; 864 865 tPNGDisplay() { 866 } 867 868 869 void resetZooms() { 870 if (blankModule->imageFitEnum == 0) { 871 blankModule->zoomX = blankModule->width / imgWidth; 872 blankModule->zoomY = blankModule->height / imgHeight; 873 blankModule->xOffset = 0; 874 blankModule->yOffset = 0; 875 876 } else if (blankModule->imageFitEnum == 1) { // fit width 877 blankModule->zoomX = blankModule->width / imgWidth; 878 blankModule->zoomY = blankModule->zoomX; 879 blankModule->xOffset = 0; 880 blankModule->yOffset = 0; 881 882 } else if (blankModule->imageFitEnum == 2) { //fit height 883 blankModule->zoomY = blankModule->height / imgHeight; 884 blankModule->zoomX = blankModule->zoomY; 885 blankModule->xOffset = 0; 886 blankModule->yOffset = 0; 887 } 888 else if (blankModule->imageFitEnum == 3) { 889 890 } 891 } 892 893 void setOffsets() { 894 } 895 void drawLayer(const BGPanel::DrawArgs& args, int layer) override { 896 if (layer == 1 && lightWidgetMode) { 897 drawImage(args); 898 } 899 Widget::drawLayer(args, layer); 900 } 901 void draw(const rack::Widget::DrawArgs &args) override { 902 if (!lightWidgetMode) { 903 drawImage(args); 904 } 905 } 906 907 void drawImage(const BGPanel::DrawArgs& args) { 908 if (blankModule && blankModule->loadedJSON) { 909 std::string modulePath = blankModule->getPath(); 910 if (path != modulePath) { 911 gifBuddy = AnimatedGifBuddy(args.vg, modulePath.c_str()); 912 913 if (gifBuddy.getImageStatus() == 3) { 914 std::string badGifPath = asset::plugin(pluginInstance, "res/broken-file.gif"); 915 gifBuddy = AnimatedGifBuddy(args.vg, badGifPath.c_str()); 916 missingOrBroken = true; 917 } 918 else { 919 missingOrBroken = false; 920 } 921 img = gifBuddy.getHandle(); 922 int numImageFrames = gifBuddy.getFrameCount(); 923 924 blankModule->setFrameCount(numImageFrames); 925 926 //if this check isnt performed, windows crashes with non-gifs due to 927 //the call to vector insert 928 if (numImageFrames > 1) { 929 blankModule->setFrameCount(gifBuddy.getFrameCount()); 930 blankModule->setFrameDelays(gifBuddy.getAllFrameDelaysSeconds()); 931 blankModule->setTotalGifDuration(gifBuddy.getTotalGifDuration()); 932 blankModule->setTotalGifDurationIfInPingPongMode(gifBuddy.getPingPongGifDuration()); 933 blankModule->setFrameDelay(gifBuddy.getSecondsDelay(0)); 934 } 935 936 int imageStatus = gifBuddy.getImageStatus(); 937 blankModule->setImageStatus(imageStatus); 938 939 //couldnt load file 940 blankModule->setContainingDirectory(); 941 blankModule->setReady(true); 942 943 944 nvgImageSize(args.vg, img, &imgWidth, &imgHeight); 945 imgRatio = ((float)imgWidth / (float)imgHeight); 946 947 /* 948 1) user selects image from dialog: reset zooms 949 2) change came from slideshow: reset zooms 950 951 3) loaded from JSON dont reset zooms 952 */ 953 954 if (blankModule->jsonFlag && !missingOrBroken) { 955 //dont want to reset zooms if loading from json 956 //unsure of another way to distinguish (1) from (3) 957 //other than this janky flag 958 blankModule->jsonFlag = false; 959 } else { 960 resetZooms(); 961 } 962 963 path = modulePath; 964 965 } 966 967 if (blankModule->imageFitEnum != lastEnum && lastEnum != -1) { 968 lastEnum = blankModule->imageFitEnum; 969 resetZooms(); 970 } 971 lastEnum = blankModule->imageFitEnum; 972 if (!path.empty() && path != "empty") { 973 nvgBeginPath(args.vg); 974 NVGpaint imgPaint; 975 nvgScale(args.vg, blankModule->zoomX, blankModule->zoomY); 976 imgPaint = nvgImagePattern(args.vg, blankModule->xOffset, blankModule->yOffset, imgWidth, imgHeight, 0, img, 1.0f); 977 nvgRect(args.vg, blankModule->xOffset, blankModule->yOffset, imgWidth, imgHeight); 978 nvgFillPaint(args.vg, imgPaint); 979 nvgFill(args.vg); 980 nvgClosePath(args.vg); 981 } 982 if (!blankModule->pauseAnimation) { 983 gifBuddy.displayGifFrame(args.vg, currentFrame); 984 } 985 } 986 } 987 void step() override { 988 if (blankModule && blankModule->loadedJSON) { 989 if (blankModule->mappedFrame != currentFrame) { 990 currentFrame = blankModule->mappedFrame; 991 } 992 if (blankModule->scrubbing) { 993 currentFrame = blankModule->scrubFrame; 994 } 995 } 996 TBase::step(); 997 } 998 }; 999 1000 1001 typedef tPNGDisplay<TransparentWidget> PNGDisplayTransparentWidget; 1002 1003 struct PNGDisplay : Widget { 1004 int counter = 0; 1005 bool lightWidgetMode = false; 1006 PNGDisplay(ComputerscareBlank *blankModule) { 1007 module = blankModule; 1008 1009 pngTransparent = new PNGDisplayTransparentWidget(); 1010 pngTransparent->blankModule = blankModule; 1011 1012 addChild(pngTransparent); 1013 Widget(); 1014 } 1015 void resetZooms() { 1016 pngTransparent->resetZooms(); 1017 } 1018 void step() override { 1019 if (module) { 1020 bool moduleLightWidgetMode = module->getLightWidgetMode(); 1021 if (moduleLightWidgetMode != lightWidgetMode) { 1022 lightWidgetMode = moduleLightWidgetMode; 1023 pngTransparent->lightWidgetMode = lightWidgetMode; 1024 } 1025 } 1026 Widget::step(); 1027 } 1028 PNGDisplayTransparentWidget *pngTransparent; 1029 ComputerscareBlank *module; 1030 }; 1031 1032 1033 struct GiantFrameDisplay : TransparentWidget { 1034 ComputerscareBlank *module; 1035 SmallLetterDisplay *description; 1036 SmallLetterDisplay *frameDisplay; 1037 GiantFrameDisplay() { 1038 box.size = Vec(200, 380); 1039 1040 description = new SmallLetterDisplay(); 1041 description->value = "Frame Zero, for EOC output, reset input, and sync mode"; 1042 description->fontSize = 24; 1043 description->breakRowWidth = 200.f; 1044 description->box.pos.y = box.size.y - 130; 1045 1046 1047 frameDisplay = new SmallLetterDisplay(); 1048 frameDisplay->fontSize = 90; 1049 frameDisplay->box.size = Vec(300, 120); 1050 frameDisplay->textOffset = Vec(0, 50); 1051 frameDisplay->box.pos.y = box.size.y - 200; 1052 frameDisplay->breakRowWidth = 200.f; 1053 frameDisplay->baseColor = nvgRGBAf(0.8, 0.8, 0.8, 0.7); 1054 1055 1056 1057 addChild(frameDisplay); 1058 addChild(description); 1059 1060 TransparentWidget(); 1061 } 1062 void step() { 1063 if (module) { 1064 visible = module->scrubbing; 1065 frameDisplay->value = string::f("%i / %i", module->scrubFrame + 1, module->numFrames); 1066 } 1067 else { 1068 visible = false; 1069 } 1070 TransparentWidget::step(); 1071 } 1072 void draw(const DrawArgs &args) { 1073 if (module) { 1074 TransparentWidget::draw(args); 1075 } 1076 } 1077 }; 1078 1079 struct ComputerscareBlankWidget : ModuleWidget { 1080 ComputerscareBlankWidget(ComputerscareBlank *blankModule) { 1081 1082 setModule(blankModule); 1083 if (blankModule) { 1084 this->blankModule = blankModule; 1085 box.size = Vec(blankModule->width, blankModule->height); 1086 } else { 1087 box.size = Vec(8 * 15, 380); 1088 } 1089 { 1090 BGPanel *bgPanel = new BGPanel(nvgRGB(0xE0, 0xE0, 0xD9)); 1091 bgPanel->box.size = box.size; 1092 this->bgPanel = bgPanel; 1093 addChild(bgPanel); 1094 ComputerscareSVGPanel *panel = new ComputerscareSVGPanel(); 1095 panel->box.size = box.size; 1096 panel->setBackground(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ComputerscareCustomBlankPanel.svg"))); 1097 this->panel = panel; 1098 addChild(panel); 1099 1100 } 1101 1102 if (blankModule) { 1103 { 1104 PNGDisplay *pngDisplay = new PNGDisplay(blankModule); 1105 pngDisplay->box.pos = Vec(0, 0); 1106 1107 pngDisplay->box.size = Vec( blankModule->width , blankModule->height ); 1108 1109 this->pngDisplay = pngDisplay; 1110 addChild(pngDisplay); 1111 } 1112 } 1113 ComputerscareResizeHandle *leftHandle = new ComputerscareResizeHandle(); 1114 1115 ComputerscareResizeHandle *rightHandle = new ComputerscareResizeHandle(); 1116 rightHandle->right = true; 1117 this->rightHandle = rightHandle; 1118 addChild(leftHandle); 1119 addChild(rightHandle); 1120 1121 frameDisplay = new GiantFrameDisplay(); 1122 frameDisplay->module = blankModule; 1123 addChild(frameDisplay); 1124 1125 } 1126 1127 void appendContextMenu(Menu* menu) override { 1128 ComputerscareBlank* blank = dynamic_cast<ComputerscareBlank*>(this->blankModule); 1129 1130 1131 modeMenu = new ParamSelectMenu(); 1132 modeMenu->text = "Animation Mode"; 1133 modeMenu->rightText = RIGHT_ARROW; 1134 modeMenu->param = blankModule->paramQuantities[ComputerscareBlank::ANIMATION_MODE]; 1135 modeMenu->options = blankModule->animationModeDescriptions; 1136 1137 endMenu = new ParamSelectMenu(); 1138 endMenu->text = "Slideshow / Next File Behavior"; 1139 endMenu->rightText = RIGHT_ARROW; 1140 endMenu->param = blankModule->paramQuantities[ComputerscareBlank::NEXT_FILE_BEHAVIOR]; 1141 endMenu->options = blankModule->nextFileDescriptions; 1142 1143 menu->addChild(new MenuEntry); 1144 1145 1146 menu->addChild(modeMenu); 1147 menu->addChild(endMenu); 1148 1149 KeyboardControlChildMenu *kbMenu = new KeyboardControlChildMenu(); 1150 kbMenu->text = "Keyboard Controls"; 1151 kbMenu->rightText = RIGHT_ARROW; 1152 kbMenu->blank = blank; 1153 menu->addChild(kbMenu); 1154 1155 1156 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "")); 1157 LoadImageItem* loadImageItem = createMenuItem<LoadImageItem>("Load image (PNG, JPEG, BMP, GIF)"); 1158 loadImageItem->blankModule = blank; 1159 menu->addChild(loadImageItem); 1160 1161 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Current Image Path:")); 1162 menu->addChild(construct<MenuLabel>(&MenuLabel::text, blank->getPath())); 1163 1164 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "")); 1165 1166 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Image Scaling")); 1167 menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Both (stretch both directions)", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 0)); 1168 menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Width", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 1)); 1169 menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Fit Height", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 2)); 1170 menu->addChild(construct<ImageFitModeItem>(&MenuItem::text, "Free", &ImageFitModeItem::blank, blank, &ImageFitModeItem::imageFitEnum, 3)); 1171 1172 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "")); 1173 1174 MenuToggle* animEnabled = new MenuToggle(blank->paramQuantities[ComputerscareBlank::ANIMATION_ENABLED]); 1175 menu->addChild(animEnabled); 1176 1177 MenuToggle* constantDelay = new MenuToggle(blank->paramQuantities[ComputerscareBlank::CONSTANT_FRAME_DELAY]); 1178 menu->addChild(constantDelay); 1179 1180 MenuToggle* slideshowEnabled = new MenuToggle(blank->paramQuantities[ComputerscareBlank::SLIDESHOW_ACTIVE]); 1181 menu->addChild(slideshowEnabled); 1182 1183 MenuToggle* lightWidgetMode = new MenuToggle(blank->paramQuantities[ComputerscareBlank::LIGHT_WIDGET_MODE]); 1184 menu->addChild(lightWidgetMode); 1185 1186 menu->addChild(construct<MenuLabel>(&MenuLabel::text, "")); 1187 1188 MenuParam* speedParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::ANIMATION_SPEED], 2); 1189 menu->addChild(speedParam); 1190 1191 MenuParam* shuffleParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::SHUFFLE_SEED], 2); 1192 menu->addChild(shuffleParam); 1193 1194 MenuParam* slideshowSpeedParam = new MenuParam(blank->paramQuantities[ComputerscareBlank::SLIDESHOW_TIME], 2); 1195 menu->addChild(slideshowSpeedParam); 1196 1197 1198 } 1199 void step() override { 1200 if (module) { 1201 if (blankModule && !blankModule->loadedJSON) { 1202 box.size.x = blankModule->width; 1203 panel->box.size.x = blankModule->width; 1204 bgPanel->box.size.x = blankModule->width; 1205 panel->box.pos.x = blankModule->width / 2 - 60.f; 1206 pngDisplay->box.size.x = blankModule->width; 1207 1208 rightHandle->box.pos.x = blankModule->width - rightHandle->box.size.x; 1209 blankModule->loadedJSON = true; 1210 blankModule->jsonFlag = true; 1211 1212 } 1213 else { 1214 if (box.size.x != blankModule->width) { 1215 blankModule->width = box.size.x; 1216 panel->box.pos.x = box.size.x / 2 - 60.f; 1217 pngDisplay->box.size.x = box.size.x; 1218 1219 bgPanel->box.size.x = box.size.x; 1220 rightHandle->box.pos.x = box.size.x - rightHandle->box.size.x; 1221 pngDisplay->resetZooms(); 1222 } 1223 panel->visible = blankModule->path.empty(); 1224 } 1225 ModuleWidget::step(); 1226 } 1227 }; 1228 void onHoverKey(const event::HoverKey& e) override { 1229 float dZoom = 0.05; 1230 float dPosition = 10.f; 1231 if (e.isConsumed()) 1232 return; 1233 1234 // Scroll with arrow keys 1235 /*float arrowSpeed = 30.0; 1236 if ((e.mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) 1237 arrowSpeed /= 16.0; 1238 else if ((e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) 1239 arrowSpeed *= 4.0; 1240 else if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) 1241 arrowSpeed /= 4.0;*/ 1242 //duplicate is ctrl-d, ignore keys if mods are pressed so duplication doesnt translate the image 1243 if (e.action == RACK_HELD && !e.mods ) { 1244 if (e.keyName == "a") { 1245 blankModule->xOffset += dPosition / blankModule->zoomX; 1246 e.consume(this); 1247 } else if (e.keyName == "s") { 1248 blankModule->yOffset -= (blankModule->invertY ? dPosition : -dPosition) / blankModule->zoomY; 1249 e.consume(this); 1250 } else if (e.keyName == "d") { 1251 blankModule->xOffset -= dPosition / blankModule->zoomX; 1252 e.consume(this); 1253 } else if (e.keyName == "w") { 1254 blankModule->yOffset += (blankModule->invertY ? dPosition : -dPosition) / blankModule->zoomY; 1255 e.consume(this); 1256 } else if (e.keyName == "z") { 1257 blankModule->zoomX *= (1 + dZoom); 1258 blankModule->zoomY *= (1 + dZoom); 1259 e.consume(this); 1260 } else if (e.keyName == "x") { 1261 blankModule->zoomX *= (1 - dZoom); 1262 blankModule->zoomY *= (1 - dZoom); 1263 e.consume(this); 1264 } else if (e.keyName == "q") { 1265 blankModule->rotation += 1; 1266 blankModule->rotation %= 4; 1267 e.consume(this); 1268 } else if (e.keyName == "e") { 1269 blankModule->rotation -= 1; 1270 blankModule->rotation += 4; 1271 blankModule->rotation %= 4; 1272 e.consume(this); 1273 } else if (e.keyName == "j") { 1274 blankModule->prevFrame(); 1275 e.consume(this); 1276 } else if (e.keyName == "l") { 1277 blankModule->nextFrame(); 1278 e.consume(this); 1279 } 1280 1281 } 1282 if (e.action == GLFW_RELEASE) { 1283 if (e.keyName == "k") { 1284 blankModule->goToFrame(0); 1285 e.consume(this); 1286 } else if (e.keyName == "i") { 1287 blankModule->goToRandomFrame(); 1288 e.consume(this); 1289 } else if (e.keyName == "u") { 1290 blankModule->goToRandomFrame(); 1291 e.consume(this); 1292 } else if (e.keyName == "p") { 1293 blankModule->toggleAnimationEnabled(); 1294 e.consume(this); 1295 } else if (e.keyName == "o") { 1296 blankModule->loadRandomGif(); 1297 e.consume(this); 1298 } else if (e.keyName == "[") { 1299 blankModule->prevFileInCatalog(); 1300 e.consume(this); 1301 } else if (e.keyName == "]") { 1302 blankModule->nextFileInCatalog(); 1303 e.consume(this); 1304 } 1305 } 1306 ModuleWidget::onHoverKey(e); 1307 } 1308 ComputerscareBlank *blankModule; 1309 PNGDisplay *pngDisplay; 1310 ComputerscareSVGPanel *panel; 1311 BGPanel *bgPanel; 1312 TransparentWidget *display; 1313 ComputerscareResizeHandle *leftHandle; 1314 ComputerscareResizeHandle *rightHandle; 1315 GiantFrameDisplay *frameDisplay; 1316 ParamSelectMenu *modeMenu; 1317 ParamSelectMenu *endMenu; 1318 }; 1319 1320 1321 1322 Model *modelComputerscareBlank = createModel<ComputerscareBlank, ComputerscareBlankWidget>("computerscare-blank");