Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

1423
LINES

< > BotCompany Repo | #1035021 // Gazelle Screen Cam / Gazelle 22 Module [backup]

JavaX source code (Dynamic Module) - run with: Stefan's OS

!7

need latest area.

// We're banning db_mainConcepts() again to allow multiple databases loaded at once

// !transpileMainSnippet #1033860

!include once #1033998 // RSyntaxTextArea
!include once #1013490 // sarxos webcam

!include early #1034876 // Gazelle 22 Flags [Include]

!include early #1033884 // Compact Module Include Gazelle V [dev version]

!include once #1034034 // Gazelle 22 Function Include for Scripts

rewrite Challenge to G22Challenge.
rewrite Label to G22Label.
rewrite GalleryImage to G22GalleryImage.

module GazelleScreenCam is G22ProjectActions {
  !include early #1025212 // +Enabled without visualization
  { enabled = false; }

  transient static bool autoRunSelfTests = true;
  
  int pixelRows = 128, colors = 8;
  S script = "64p 8c gradientImage";
  S newScript;
  S screenCamScript;
  S selectedTab;
  S javaCode;
  bool horizontalLayout; // flat layout
  int fpsTarget = 20;
  S webCamName;
  bool webCamAccessEnabled = true;
  
  WatchTarget watchTarget;
  
  new G22_TestScreenPanel testScreen;

  // The following fields related to the screen cam panel
  transient ImageSurface isPosterized;
  transient new ScreenCamStream imageStream;

  transient Gazelle22_ImageToRegions imageToRegions_finished;
  transient new DoubleFPSCounter fpsCounter;
  transient int fps;
  transient WatchTargetSelector watchTargetSelector;
  transient new RollingAverage remainingMSPerFrame;
  transient int remainingMS;
  // End of fields for screen cam panel
  
  transient new FunctionTimings<S> functionTimings;
  
  transient ReliableSingleThread rstRunScript = dm_rst(me(), r _runScript);
  
  transient JGazelleVScriptRunner scriptRunner;
  
  transient UIURLSystem uiURLs;

  //transient L<Webcam> availableWebCams;
  transient JComboBox<Webcam> cbWebCam;
  transient SingleComponentPanel scpWebCamImage;
  transient WebcamPanel webCamPanel;

  transient JLeftArrowScriptIDE screenCamScriptIDE;
  transient GazelleV_LeftArrowScript.Script runningScreenCamScript;
  
  // set by host
  transient Concepts concepts;
  
  S uiURL = "Main Tabs";
  
  transient FileWatchService fileWatcher;
  
  transient SimpleCRUD_v2<Label> labelCRUD;
  transient SimpleCRUD_v2<GalleryImage> galleryCRUD;
  transient JComponent galleryCRUDVis;

  transient SimpleCRUD_v2<Entity> entityCRUD;

  transient JGallery /*gallery,*/ paintToolGallery;
  //transient ImageSurface galleryImageSurface;

  transient FlexibleRateTimer screenCamTimer;
  transient SingleComponentPanel scpMain;
  transient JTabbedPane mainTabs;
  transient bool showRegionsAsOutline = true;
  transient JComponent watchScreenPane;
  transient S screenCamRecognitionOutput;

  transient gettable new G22Utils g22utils;
  delegate stdImageSurface to g22utils.
  delegate setupScriptCRUD to g22utils.
  delegate dbDir to g22utils.

  delegate showUIURL to uiURLs.
  delegate renderUIURL to uiURLs.

  
  transient new BackgroundProcessesUI backgroundProcessesUI;
  
  transient BackgroundProcessesUI.Entry bgScreenCam = backgroundProcessesUI.new Entry("Screen Cam")
    .menuItem(jMenuItem("Screen Cam", r showScreenCam));
  transient BackgroundProcessesUI.Entry bgWebCam = backgroundProcessesUI.new Entry("Web Cam")
    .menuItem(jMenuItem("Web Cam", r showWebCam));

  bool autoRunChallenge = true;

  transient G22ChallengesPanel challengesPanel;

  transient JComponent urlBar;

  transient JVideoLibDownloader videoLibDownloader;
  transient JFFMPEGVideoPlayer videoPlayer;

  // it's now actually persistent
  settableWithVar JPaintTool paintTool;

  // also persistent
  G22AnalysisPanel paintAnalysisPanel;
  
  // to detect when the project is moved. not implemented yet
  //settableWithVar File previousConceptsDir;

  transient G22SelfTests selfTests;

  settableWithVar bool paintToolGalleryExpanded;
  
  transient JComponent screenCamTab;

  // add fields here

  start {
    g22utils.module(this);
    
    testScreen.onChange(l0 change);
    
    dm_onFieldChange horizontalLayout(
      //r dm_revisualize // deh buggy
      r dm_reload
    );
    
    /*if (concepts == null) {
      // non-standalone mode (doesn't work yet)
      printWithMS("Starting DB");
      db();
      concepts = db_mainConcepts();
    } else
      assertSame(concepts, db_mainConcepts());*/
      
    concepts.quietSave = true;

    // make indexes
    indexConceptField(concepts, GalleryImage, "path");
    indexConceptField(concepts, Example, "item");
    indexConceptFieldCI(concepts, G22Label, "name");
    indexConceptField(concepts, G22Variable, "name");

    g22utils.concepts(concepts);
    concepts.miscMapPut(G22Utils.class, g22utils);
    g22utils.backgroundProcessesUI(backgroundProcessesUI);

    // fix legacy bug
    for (c : list(concepts)) fixFieldValues(c);

    g22utils.onSettingUpParser(l1 modifyLeftArrowParser);
    g22utils.projectActions(this);
    g22utils.onSettingUpScriptIDE(ide -> {
      if (!haveMuricaPassword()) ret;
      
      var scp = scp();
      addInFront(ide.buttons(), scp);
      
      ide.varCompileResult().onChange(cr -> {
        temp enter();

        // Offer to add standard function or class to Gazelle (if authorized)
        var e = innerExceptionOfType(GazelleV_LeftArrowScriptParser.UnknownObject.class, cr?.compileError);
        if (e != null) {
          // An unknown object is referenced in the source. Let's hunt for it.
          
          if (isStandardFunction(e.name))
            ret with scp.set(jbuttonWithDisable("Add function " + e.name, r {
              infoBoxAndCompile(addFunctionNameToInclude(functionsIncludeID(), e.name));
            }));
            
          if (isStandardClass(e.name))
            ret with scp.set(jbuttonWithDisable("Add class " + e.name, r {
              infoBoxAndCompile(addClassNameToInclude(functionsIncludeID(), e.name));
            }));
        }
 
        scp.clear();
      });
    });

    g22utils.autoStarter().init();

    //g22utils.basicParserTest();
    
    // update count in tab when tab isn't selected
    onConceptChangeByClass(concepts, Label, -> {
      labelCRUD?.updateEnclosingTabTitle();
      //galleryCRUD?.updateEnclosingTabTitle();
    });
    
    uiURLs = new UIURLSystem(me(), dm_fieldLiveValue uiURL());
    uiURLs.addPreferredUIURL("Main Tabs");

    dm_watchFieldAndNow enabled(-> backgroundProcessesUI.addOrRemove(enabled, bgScreenCam));
    
    scriptRunner = new JGazelleVScriptRunner(dm_fieldLiveValue script(me()));

    printWithMS("Making image stream");

    imageStream.onNewElement(img -> {
      fpsCounter.inc();
      setField(fps := iround(fpsCounter!));

      // perform analysis on screen cam image

      // new G22_ImageAnalysis screenCamImageAnalysis;
      
      assertTrue("meta", getMetaSrc(img) instanceof ScreenShotMeta);
      Gazelle22_ImageToRegions itr = new(functionTimings, img, new SnPSettings(pixelRows, colors));
      itr.run();
      imageToRegions_finished = itr;

      // run "linear" script on image if any
      
      if (shouldRunScript()) rstRunScript.go();

      // run left-arrow script

      if (screenCamScriptIDE != null 
        && screenCamScriptIDE.visible()) try {
          g22_runPostAnalysisLeftArrowScript(itr, runningScreenCamScript);
        } catch e {
          printStackTrace(e);
          screenCamScriptIDE.showRuntimeError(e);
        }

      // make textual result of analysis
      
      S text = nRegions(itr.regions.regionCount());
      setField(screenCamRecognitionOutput := text);
      
      // display image after analysis so we can highlight a region

      g22_renderPosterizedHighlightedImage(isPosterized, itr, showRegionsAsOutline);
    });

    watchTargetSelector = new WatchTargetSelector;

    /*if (webCamAccessEnabled) {
      printWithMS("Detecting web cams");
      watchTargetSelector.updateCamCount();
      printWithMS("Found " + n2(availableWebCams, "web cam"));
    }*/

    printWithMS("Starting screen cam");
    
    ownResource(screenCamTimer = new FlexibleRateTimer(fpsTarget, rEnter {
      if (!enabled) ret;
      watchTargetSelector?.updateScreenCount();
      Timestamp deadline = tsNowPlusMS(1000/fpsTarget);
      watchTarget?.mainWindow(getWindow(urlBar));
      watchTarget?.configureScreenCamStream(imageStream);
      imageStream.step();
      long remaining = deadline.minus(tsNow());
      remainingMSPerFrame.add(remaining);
      setField(remainingMS := iround(remainingMSPerFrame!));
    }));
    screenCamTimer.start();
    dm_onFieldChange fpsTarget(-> screenCamTimer.setFrequencyImmediately(fpsTarget));

    printWithMS("Gathering images from disk");
    //addDirToGallery(galleryDir()));
    addDirToGallery(conceptsDir(concepts));
    printWithMS("Got dem images");

    transpileRaw_makeTranslator = -> hotwire(#7);

    // OSHI adds 4.5 MB to the jar...
    //doAfter(5.0, r { print("Process size: ", toM_str(oshi_currentProcessResidentSize_noZGCFix())) });
  } // end of start method

  void addDirToGallery(File dir) {
    watchDirForGallery(dir);
    for (f : allImageFiles(dir))
      addToGallery(f);
  }
  
  void watchDirForGallery(File dir) {
    fileWatcher if null = new FileWatchService;
    fileWatcher.addRecursiveListener(dir, file -> {
      if (isImageFile(file))
        addToGallery(file);
    });
  }

  // main window visualization
  cachedVisualize {
    isPosterized = stdImageSurface();

    // if screen cam is not enabled, highlight regions of last captured image
    // on mouse hover
    
    imageSurfaceOnHover(isPosterized, pt -> {
      if (!enabled && imageToRegions_finished != null)
        g22_renderPosterizedHighlightedImage(isPosterized, imageToRegions_finished,
          showRegionsAsOutline);
    });
        
    labelCRUD = new SimpleCRUD_v2(concepts, Label);
    labelCRUD.hideFields("globalID");
    labelCRUD.addCountToEnclosingTab(true);
    labelCRUD.itemToMap_inner2 = l
      -> litorderedmap(
        "Name" := l.name,
        "# Examples" := n2(countConcepts(concepts, Example, label := l)));
    
    galleryCRUD = new SimpleCRUD_v2(concepts, GalleryImage);
    galleryCRUD.addCountToEnclosingTab(true);
    galleryCRUD.itemToMap_inner2 = img
      -> litorderedmap(
        "File" := fileName(img.path),
        "Folder" := dirPath(img.path));
    galleryCRUD.defaultAction(img -> { thread { showImage(img.path) }});
    galleryCRUDVis = galleryCRUD.visualize();
    galleryCRUD.addButton(jPopDownButton_noText(
      "Forget missing images" := rThreadEnter forgetMissingImages
    ));
    galleryCRUD.addButton("Back to gallery", rThreadEnter { showUIURL("Gallery") });
    galleryCRUD.newConcept = -> {
      new JFileChooser fc;
      fc.setDialogTitle("Add image to gallery");
      fc.setCurrentDirectory(conceptsDir(concepts));
      fc.setFileFilter(new ImageFileFilter().allowDirectories(true));
      addToGallery(execFileChooser(fc));
    };
    
    // main visual

    watchScreenPane = borderlessScrollPane(jHigherScrollPane(
      jfullcenter(vstack(
        withLeftAndRightMargin(hstack(
          dm_rcheckBox enabled("Watch"),
          watchTargetSelector.visualize(),
          jlabel(" in "),
          withLabelToTheRight("colors @ ", dm_spinner colors(2, 256)),
          withLabelToTheRight("p", dm_powersOfTwoSpinner pixelRows(512)),
        )),
        verticalStrut(2),
        withSideMargins(centerAndEastWithMargin(
          dm_fieldLabel screenCamRecognitionOutput(),
          dm_transientCalculatedToolTip speedInfo_long(rightAlignLabel(dm_transientCalculatedLabel speedInfo()))
        )),
      ))));

    initUIURLs();

    mainTabs = scrollingTabs(jTopOrLeftTabs(horizontalLayout));

    addUIURLToMainTabs("Screen Cam");
    addUIURLToMainTabs("Project Overview");
    addUIURLToMainTabs("Paint");
    addUIURLToMainTabs("Analyzers");
    addUIURLToMainTabs("Scripts");
    addUIURLToMainTabs("Scratchpad");
    addUIURLToMainTabs("Gallery");
    addUIURLToMainTabs("Projects");

    // add tabs here

    // add main tabs to UI URLs (probably don't need this anymore)
    for (S tab : tabNames(mainTabs)) {
      reMutable tab = dropTrailingBracketedCount(tab);
      uiURLs.put(tab, -> {
        int i = indexOfTabNameWithoutTrailingCount(mainTabs, tab);
        if (i < 0) ret jcenteredlabel("Hmm. Tab not found");
        selectTab(mainTabs, i);
        ret mainTabs;
      });
    }

    // Fix UI URL after tab was changed manually
    // Hopefully doesn't brake stuff (seems it doesn't)
    onTabSelected(mainTabs, -> {
      if (!isShowing(mainTabs)) ret;
      S tabName = dropTrailingBracketedCount(selectedTabName(mainTabs));
      if (!eqicOneOf(uiURL, "Main Tabs", tabName))
        uiURLs.showUIURL("Main Tabs");
        //uiURLs.showUIURL(tabName);
    });

    var cbEnabled = toolTip("Switch screen cam on or off", dm_checkBox enabled(""));
    var lblScreenCam = setToolTip("Show scaled down and color-reduced screen image",
      jlabel("Screen Cam"));
    tabComponentClickFixer(lblScreenCam);
    screenCamTab = hstackWithSpacing(cbEnabled, lblScreenCam);
    addActionListener(cbEnabled, -> { if (enabled) selectTabComponent(mainTabs, screenCamTab); });
    
    replaceTabTitleComponent(mainTabs, "Screen Cam", screenCamTab);
    
    // for tab titles
    challengesPanel?.updateCount();
    labelCRUD?.update();
    galleryCRUD?.update();
    analyzersPanel().addCountToEnclosingTab(true);
    analyzersPanel().updateCount();
    scriptsPanel().addCountToEnclosingTab(true);
    scriptsPanel().updateCount();
    
    persistSelectedTabAsLiveValue(mainTabs, dm_fieldLiveValue selectedTab());

    urlBar = uiURLs.urlBar();
    focusOnFirstShow(urlBar);
    setToolTip(uiURLs.comboBox, "UI navigation system");

    scpMain = singleComponentPanel();
    uiURLs.scp(scpMain);

    showUIURL(uiURL);

    var lblDB = toolTip("Currently selected project (" + f2s(concepts.conceptsDir()) + ")",
      jSimpleLabel(conceptsDirName(concepts)));
    componentPopupMenuItems(lblDB,
      "Manage or open projects...",
        rThread { showUIURL("Projects")});
    
    var vis = northAndCenter(
      withSideAndTopMargin(
        westCenterAndEast(
          //withLabelLeftAndRight("DB:", lblDB, "| "),
          withLabelToTheRight(lblDB, "| "),
          urlBar,
          withLeftMargin(backgroundProcessesUI.shortLabel()))),
      scpMain,
    );

    g22utils.autoStarter().start();

    if (autoRunSelfTests) {
      autoRunSelfTests = false;
      onFirstShow(vis, l0 runSelfTests);
    }

    ret vis;
  }

  void runSelfTests {
    thread "Self-Tests" {
      selfTests = new G22SelfTests(g22utils);
      selfTests.run();
      selfTestsDone();
    }
  }

  event selfTestsDone;

  JComponent screenCamPanel() {
    ret centerAndSouthOrEast(horizontalLayout,
      withTools(isPosterized),
      watchScreenPane);
  }
  
  JComponent screenCamPlusScriptPanel() enter {
    print("screenCamPlusScriptPanel");
    //ret watchScreenPane;
    try {
      var ide = screenCamScriptIDE = leftArrowScriptIDE();
      ide.runButtonShouldBeEnabled = ->
         eq(getText(ide.btnRun), "Stop")
          || ide.runButtonShouldBeEnabled_base();
      
      ide.runScript = -> {
        if (eq(getText(ide.btnRun), "Stop"))
          runningScreenCamScript = null;
        else {
          runningScreenCamScript = screenCamScriptIDE.parsedScript();
          ide.showStatus("Running");
        }
        setText(ide.btnRun,
          runningScreenCamScript == null ? "Run" : "Stop");
      };
      
      ret centerAndSouthOrEast(horizontalLayout,
        //withTools(
          jhsplit(
            jscroll_centered_borderless(isPosterized),
            screenCamScriptIDE
              .lvScript(dm_fieldLiveValue screenCamScript())
              .visualize())
        //, isPosterized)
        , watchScreenPane);
    } on fail e { print(e); }
  }
  
  S speedInfo() {
    ret "FPS " + fps + " idle " + remainingMS + " ms";
  }
  
  S speedInfo_long() {
    ret "Screen cam running at " + nFrames(fps) + "/second. " + n2(remainingMS) + " ms remaining per frame in first core"
      + " (of targeted " + fpsTarget + " FPS)";
  }
  
  bool useErrorHandling() { false; }
  
  S renderFunctionTimings() {
    ret lines(ciSorted(map(functionTimings!, (f, avg) ->
      firstToUpper(f) + ": " + n2(iround(nsToMicroseconds(avg!))) + " " + microSymbol() + "s (" + n2(iround(avg.n())) + ")")));
  }
  
  transient long _runScript_idx;

  void _runScript() {
    scriptRunner.parseAndRunOn(imageToRegions_finished.ii);
  }
  
  bool shouldRunScript() {
    ret isShowing(scriptRunner.scpScriptResult);
  }
  
  JComponent testScreenPanel() {
    ret withSideAndTopMargin(testScreen.visualize());
  }
  
  L popDownItems() {
    ret ll(jCheckBoxMenuItem_dyn("Horizontal Layout",
      -> horizontalLayout,
      b -> setField(horizontalLayout := b)));
  }
  
  void unvisualize {} // don't zero transient component fields
  
  void resetTimings { functionTimings.reset(); }
  
  // add tool side bar to image surface
  JComponent withTools(
    JComponent component default jscroll_centered_borderless(is),
    ImageSurface is) {
    ret centerAndEastWithMargin(component,
      vstack(
        verticalStrut(5),
        jimageButtonScaledToWidth(16, #1103054, "Save screenshot in gallery", rThread saveScreenshotToGallery),
        /*jPopDownButton_noText(
          jCheckBoxMenuItem_dyn("Live scripting", -> liveScripting, b -> setLiveScripting(b)
        ),*/
      )
    );
  }
  
  class WatchTargetSelector {
    JComboBox<WatchTarget> cb = jComboBox();
    int screenCount, camCount;
    
    visualize {
      updateList();
      //print("Selecting watchTarget: " + watchTarget);
      selectItem(cb, watchTarget);
      main onChange(cb, watchTarget -> {
        setField(+watchTarget);
        //print("Chose watchTarget: " + GazelleScreenCam.this.watchTarget);
      });
      ret cb;
    }
    
    void updateScreenCount() {
      if (screenCount != screenCount())
        updateList();
    }

    void updateList() swing {
      setComboBoxItems(cb, makeWatchTargets());
    }
    
    L<WatchTarget> makeWatchTargets() {
      ret flattenToList(
        countIteratorAsList_incl(1, screenCount = screenCount(),
          i -> WatchScreen(i)),
        new WatchMouse,
        new WatchScreenWithMouse,
        new WatchOtherScreen,
      );
    }
  }

  JComponent wrapCRUD(S title, SimpleCRUD_v2 crud, JComponent vis default crud.visualize()) {
    ret crud == null ?: withTopAndBottomMargin(jCenteredRaisedSection(title, withMargin(vis)));
  }
  
  File galleryDir() {
    ret picturesDir(gazelle22_imagesSubDirName());
  }
  
  void saveScreenshotToGallery enter {
    var img = imageStream!;
    addToGallery(saveImageWithCounter(galleryDir(), "Screenshot", img));
  }
  
  void saveWebCamImageToGallery enter {
    var img = webCamPanel.getImage();
    addToGallery(saveImageWithCounter(galleryDir(), "Webcam", img));
  }
  
  GalleryImage addToGallery(File imgFile) {
    if (!isImageFile(imgFile)) null;
    var img = uniq(concepts, GalleryImage, path := imgFile);
    printVars("addToGallery", +imgFile, +img);
    ret img;
  }

  class StudyPanel {
    new G22SnPSelector snpSelector;
    GalleryImage image;
    BufferedImage originalImage;
    //ImageSurface isOriginal = stdImageSurface();
    ImageSurface isOriginalWithRegions = stdImageSurface();
    ImageSurface isPosterized = stdImageSurface();
    SingleComponentPanel scp = singleComponentPanel();
    SingleComponentPanel analysisPanel = singleComponentPanel();
    SingleComponentPanel scpExampleCRUD = singleComponentPanel();
    ConceptsComboBox<GalleryImage> cbImage = swing(->
      new ConceptsComboBox<GalleryImage>(concepts, GalleryImage));
    Gazelle22_ImageToRegions itr;
    int iSelectedRegion;
    SimpleCRUD_v2<SavedRegion> regionCRUD;
    SimpleCRUD_v2<Example> exampleCRUD;

    *() {
      print("Making StudyPanel " + this);
      //cbImage.sortTheList = l -> sortConceptsByIDDesc(l);
      cbImage.sortTheList = l -> sortedByComparator(l, (a, b)
        -> cmpAlphanumIC(fileName(a.path), fileName(b.path)));

      main onChangeAndNow(cbImage, img -> {
        /*print("Image selected 1: " + img);
        print("Me: " + me() + ", q thread: " + me().q().hasThread()
          + ", q jobs done: " + me().q().nJobsDone());
        print("Module queue current job: " + me().q().currentJob());*/
        dm_q(me(), r {
          print("Image selected: " + img);
          image = img;
          scp.setComponent(studyImagePanel());
        });
      });

      isPosterized.removeAllTools();
      isPosterized.onMousePositionChanged(r_dm_q(me(), l0 regionUpdate));

      imageSurfaceOnLeftMouseDown(isPosterized, pt -> dm_q(me(), r { chooseRegionAt(pt) }));

      snpSelector.onChange(r_dm_q(me(), l0 runSnP));
    }

    void chooseRegionAt(Pt p) {
      if (itr == null) ret;
      iSelectedRegion = itr.regions.regionAt(p);

      if (iSelectedRegion > 0) {
        var savedRegion = uniq_returnIfNew(concepts, SavedRegion,
          +image,
          snpSettings := snpSelector.settings.cloneMe(),
          regionIndex := iSelectedRegion);

        // it's new, add values
        if (savedRegion != null) {
          print("Saved new region!");
          var bitMatrix = toScanlineBitMatrix(itr.regions.regionBitMatrix(iSelectedRegion));
          cset(savedRegion, +bitMatrix);
        }
      }
      
      regionUpdate();
    }

    // load new image
    JComponent studyImagePanel() { 
      if (image == null) null;
      originalImage = loadImage2(image.path);
      if (originalImage == null) ret jcenteredlabel("Image not found");

      //isOriginal.setImage(originalImage);
      isOriginalWithRegions.setImage(originalImage);

      iSelectedRegion = 0;
      itr = new Gazelle22_ImageToRegions(functionTimings, originalImage, snpSelector.settings);
      runSnP();

      regionCRUD = new SimpleCRUD_v2<SavedRegion>(concepts, SavedRegion);
      regionCRUD.addFilter(+image);
      regionCRUD
        .showSearchBar(false)
        .showAddButton(false)
        .showEditButton(false)
        .iconButtons(true);
        
      regionCRUD.itemToMap_inner2 = region -> {
        var examples = conceptsWhere(concepts, Example, item := region);
        
        ret litorderedmap(
          "Pixels" := region.bitMatrix == null ? "-" : n2(region.bitMatrix.pixelCount()),
          //"Shape" := "TODO",
          "Labels" := joinWithComma(map(examples, e -> e.label)),

          // TODO: scale using snpsettings
          "Position" := region.bitMatrix == null ? "-" : region.bitMatrix.boundingBoxOfTrueBits());
      };

      var regionCRUDComponent = regionCRUD.visualize();
      regionCRUD.onSelectionChanged(l0 updateExampleCRUD);

      ret hsplit(
        jtabs(
          //"Image" := jscroll_centered_borderless(isOriginal),
          "Image + regions" := jscroll_centered_borderless(isOriginalWithRegions),
          "Posterized" := jscroll_centered_borderless(isPosterized),
        ),
        northAndCenterWithMargin(
          analysisPanel,
          hsplit(
            jCenteredSection("Saved regions", regionCRUDComponent),
            scpExampleCRUD)
          ));
    }

    void runSnP {
      if (itr == null) ret;
      itr.run();
      regionUpdate();
    }

    void regionUpdate {
      if (itr == null) ret;

      var pixels = itr.posterized.getRGBPixels();
      
      // hovering region marked green
      g22_highlightRegion(pixels, isPosterized, itr, showRegionsAsOutline);

      // selected region marked blue
      itr.regions.markRegionInPixelArray(pixels, iSelectedRegion, 0xFFADD8E6);

      var highlighted = bufferedImage(pixels, itr.posterized.getWidth(), itr.posterized.getHeight());

      isPosterized.setImage(highlighted);
      isPosterized.performAutoZoom(); // seems to be necessary for some reason

      updateAnalysis();
    }

    void updateAnalysis {
      /*new LS lines;
      lines.add(nRegions(itr.regions.regionCount()));
      lines.add("Selected region: " + iSelectedRegion);
      S text = lines_rtrim(lines);
      analysisPanel.setComponent(jLabel(text)));*/
    }

    void updateExampleCRUD {
      var region = regionCRUD.selected();
      if (region == null) ret with scpExampleCRUD.clear();
      
      exampleCRUD = new SimpleCRUD_v2<Example>(concepts, Example);
      exampleCRUD.entityName = -> "label for region";
      exampleCRUD.addFilter(item := region);
      exampleCRUD.showSearchBar(false);
      exampleCRUD.iconButtons(true);
      scpExampleCRUD.set(jCenteredSection("Labels for region",
        exampleCRUD.visualize()));
    }

    void importImage {
      new JFileChooser fc;
      if (fc.showOpenDialog(cbImage) == JFileChooser.APPROVE_OPTION) {
        File file = fc.getSelectedFile().getAbsoluteFile();
        if (!isImageFile(file)) ret with infoMessage("Not an image file");
        var img = addToGallery(file);
        waitUntil(250, 5.0, -> comboBoxContainsItem(cbImage, img));
        setSelectedItem(cbImage, img);
      }
    }

    visual
      jRaisedSection(northAndCenterWithMargins(
        centerAndEast(withLabel("Study", cbImage),
          hstack(
            jlabel(" in "),
            snpSelector.visualize(),
            horizontalStrut(10),
            jPopDownButton_noText(
              "Import image...", rThreadEnter importImage,
            ),
            )),
        scp));
  } // end of StudyPanel

  void initUIURLs {
    uiURLs.put("Main Tabs", ->
      horizontalLayout ? withMargin(mainTabs) : withSideMargin(mainTabs));
      
    uiURLs.put("Screen Cam FPS", -> jFullCenter(
      vstackWithSpacing(
        jCenteredLabel("FPS target (frames per second) for screen cam:"),
        jFullCenter(dm_spinner fpsTarget(1, 60)),
        jCenteredLabel("(You can lower this value if you have a slower computer)"))));

    uiURLs.put("Timings", -> {
      JTextArea taTimings = jTextArea_noUndo();
      awtEveryAndNow(taTimings, .5, r {
        setText(taTimings, renderFunctionTimings())
      });
      ret withRightAlignedButtons(taTimings,
        Reset := r resetTimings);
    });

    uiURLs.put("Test Screen" := -> testScreenPanel());

    //uiURLs.put("Operators" := -> operatorsPanel());

    uiURLs.put("Settings" := -> settingsPanel());

    uiURLs.put("System Info" := -> systemInfoPanel());

    uiURLs.put("Web Cam" := -> webCamPanel());

    uiURLs.put("Labels" := -> wrapCRUD("Labels", labelCRUD));

    uiURLs.put(WithToolTip("Screen Cam + Linear Script", "Run a simple (linear) Gazelle V script")
      := -> withBottomMargin(scriptRunner.scriptAndResultPanel()));
      
    uiURLs.put(WithToolTip("Scratchpad", "Write and run a \"left-arrow script\"")
      := -> withBottomMargin(jOnDemand leftArrowScriptPanel()));
      
    uiURLs.put("Screen Cam + Script" := -> jOnDemand screenCamPlusScriptPanel());

    uiURLs.put(WithToolTip("Gallery CRUD", "Gallery view with more functions (delete etc)")
      := -> wrapCRUD("Images", galleryCRUD, galleryCRUDVis));

    uiURLs.put(WithToolTip("Paint", "Paint a picture for Gazelle with your mouse!")
      := -> withBottomMargin(paintPanel()));

    uiURLs.put(WithToolTip("Video", "A little video player (Note: no sound and doesn't handle some videos)")
      := -> withBottomMargin(videoPanel()));
    
    uiURLs.put(WithToolTip("Analyzers", "Manage image analyzers here")
      := -> withBottomMargin(analyzersPanel().visualize()));

    uiURLs.put(WithToolTip("Scripts", "Manage all left-arrow scripts here")
      := -> withBottomMargin(scriptsPanel().visualize()));

    uiURLs.put("Screen Cam" := -> jOnDemand screenCamPanel())

      .put(WithToolTip("Study", "Here you can analyze gallery images")
        := -> withTopAndBottomMargin(studyPanel().visualize()))
      
      .put(WithToolTip("Java", "Write & run actual Java code")
        := -> withBottomMargin(jOnDemand javaPanel()))
        
      .put(WithToolTip("Gallery", "Gallery view with preview images")
        := -> galleryPanel().visualize())
          
      .put(WithToolTip("Challenges", "Gazelle's self-tests")
        := -> challengesPanel())

      .put(WithToolTip("Projects", "Manage/load Gazelle projects")
        := -> databasesPanel())
        
      .put(WithToolTip("Entities", "Define entities (meta-level)")
        := -> entitiesPanel())

      .put("Local Fonts" := -> localFontsPanel().visualize())

      .put("Scripts from all DBs" := -> new G22ScriptsFromAllDBsPanel(g22utils).visualize())

      .put("Files in project" := -> new G22DBFileBrowser(g22utils).visualize())

      .put("HTML Editor Test" := -> new G22HtmlEditor().visualize())

      .put("Project Overview" := -> new G22ProjectOverviewPanel(g22utils).visualize())

      .put("Story" := -> new G22ProjectStoryEditor(g22utils).visualize())

      .put("All objects by ID" := -> new G22AllConceptsPanel(g22utils).visualize())

      .put("Variables" := -> new G22VariablesPanel(g22utils).visualize())

      // add ui urls above this line
    ;
  }

  transient simplyCached G22LocalFontsPanel localFontsPanel() { ret new G22LocalFontsPanel(g22utils); }

  JComponent settingsPanel() {
    var cbMinimizeToTray = jCheckBox(isTrue(getOpt(dm_stem(), "minimizeToTray")));
    onUpdate(cbMinimizeToTray, -> dm_callStem(me(), "setMinimizeToTray", isChecked(cbMinimizeToTray)));
    ret jscroll(makeForm3( 
      "Minimize to tray", toolTip("Remove Gazelle from task bar when minimized (click Gazelle icon in system tray to reactivate)", cbMinimizeToTray),
      "Access web cams", dm_checkBox webCamAccessEnabled(),
    ));
  }

  File gazelleJar() {
    ret getBytecodePathForClass(this);
  }

  JComponent systemInfoPanel() {
    var gazelleJar = gazelleJar();
    var cbMinimizeToTray = jCheckBox(isTrue(getOpt(dm_stem(), "minimizeToTray")));
    onUpdate(cbMinimizeToTray, -> dm_callStem(me(), "setMinimizeToTray", isChecked(cbMinimizeToTray)));
    // Sadly, even jScrollVertical causes a bug in formLayouter1 that
    // makes the form grow horizontally without bounds
    ret /*jscrollVertical*/(makeForm3( 
      "Java Version", jlabel(javaVersion()),
      "Gazelle Jar", JFilePathLabel(gazelleJar).visualize(),
      "Gazelle Jar Size", str_toMB_oneDigit(fileSize(gazelleJar)),
      "Memory use (objects)", jLabelShortCalcedEvery(1.0, -> str_toMB(usedMemory())),
      "Using Custom Classloader", yesNoShort(usingStarter(mc())),
      "Compilation Date", jlabel(or2(loadTextFileResource(classLoader(this), "compilation-date.txt"), "unknown")),
      "Gazelle Count" := toolTip("How many Gazelles are running in the world",
        jLiveValueLabel((LiveValue) dm_callOSOpt lvComputerCount())),
      "Gazelle Database" := JFilePathLabel(concepts.conceptsFile()).visualize(),
      "Gazelle Database Size" := str_toKB(fileSize(concepts.conceptsFile())),
      "Self-Tests" := jlabel(selfTests == null ? "Not run" : selfTests.status()),
    ));
  }

  JComponent operatorsPanel() {
    ret jcenteredlabel("TODO");
  }

  JLeftArrowScriptIDE leftArrowScriptIDE() {
    ret g22utils.leftArrowIDE();
  }

  void modifyLeftArrowParser(GazelleV_LeftArrowScriptParser parser) {
    parser.allowTheWorld(me(), mc(), utils.class, g22utils);
  }

  JComponent leftArrowScriptPanel() {
    var ide = leftArrowScriptIDE();

    var btnSave = jImageButton(#1103084, "Save script", rThreadEnter saveAsNewScript);
    
    // place to the right of hideable stuff
    int idx = 0;
    while (getComponentAtIndex(ide.buttons(), idx) instanceof SingleComponentPanel) idx++;
    addComponentAtIndex(ide.buttons(), idx, btnSave);
      
    ide.sectionTitle("Left Arrow Script Scratchpad");
    ide.lvScript(dm_fieldLiveValue newScript());
    ret withTopMargin(ide.visualize());
  }

  void saveAsNewScript {
    S text = newScript;
    inputText("Name for this script", description -> {
      temp enter();
      
      // Note: This only works like this, i.e. when showUIURL is called
      // first.
      
      showUIURL("Scripts");
      var script = cnewUnlisted(G22LeftArrowScript, +description, +text);
      scriptsPanel().selectAfterUpdate(script);
      concepts.register(script);
    });
  }

  JComponent javaPanel() enter {
    var scpResult = singleComponentPanel();
    
    new JMiniJavaIDE ide;
    ide.stringifier(g22utils.stringifier);
    ide.extraClassMembers = script -> {
      LS tok = javaTok(script);

      if (contains(tok, "draw")) ret [[
        import java.awt.*;
        import java.awt.image.*;
        
        interface SimpleRenderable {
          void renderOn(Graphics2D g);
        }
        
        static BufferedImage draw(int w, int h, SimpleRenderable r) {
          BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
          Graphics2D g = img.createGraphics();
          g.setColor(Color.white);
          g.fillRect(0, 0, w, h);
          g.setColor(Color.black);
          r.renderOn(g);
          return img;
        }
      ]];
        
      ret "";
    };

    ide.callCompiledObject = o -> {
      O result = ide.callCompiledObject_base(o);
      if (result cast BufferedImage)
        scpResult.set(jscroll_centered_borderless(stdImageSurface(result)));
      ret result;
    };
    
    ide.lvScript(dm_fieldLiveValue javaCode());
    ret jhsplit(
      jCenteredSection("Image will show here", scpResult),
      ide.visualize());
  }

  JComponent webCamPanel() {
    if (cbWebCam == null) {
      fixContextClassLoader();
      var cams = listWebCams();
      cbWebCam = jTypedComboBox(cams,
        findWebCamByName(cams, webCamName));
    }

    if (scpWebCamImage == null) scpWebCamImage = singleComponentPanel();
    
    ret northAndCenterWithMargins(
      centerAndEastWithMargin(
        withLabel("Cam", cbWebCam),
        jline(
          jbutton("Start", rThreadEnter startWebCam),
          jbutton("Stop", rThreadEnter stopWebCam)
        )),
      scpWebCamImage);
  }

  void stopWebCam {
    if (webCamPanel != null) {
      webCamPanel.stop();
      webCamPanel = null;
      scpWebCamImage?.clear();
      backgroundProcessesUI.remove(bgWebCam);
    }
  }

  void startWebCam {
    temp tempInfoBox("Starting Web Cam");
    stopWebCam();
    var cam = getSelectedItem_typed(cbWebCam);
    setField(webCamName := cam?.getName());
    if (cam != null) {
      scpWebCamImage.set(
        northAndCenterWithMargin(
          rightAlignedLine(
            jimageButtonScaledToWidth(16, #1103054, "Save web cam image in gallery", rThread saveWebCamImageToGallery)
          ),
          webCamPanel = new WebcamPanel(cam, true)
      ));
      backgroundProcessesUI.add(bgWebCam);
    }
  }
  
  void forgetMissingImages {
    int n = l(deleteConcepts(GalleryImage, img -> !img.imageExists()));
    infoBox(n == 0 ? "Nothing to forget" : "Forgot " + nImages(n));
  }

  void addUIURLToMainTabs(S url) swing {
    if (containsTabNameWithoutTrailingCount(mainTabs, url)) ret;
    if (!uiURLs.hasURL(url)) ret with print("URL not found: " + url);
    
    addTab(mainTabs, WithToolTip(uiURLs.toolTipForURL(url), url),
      uiURLs.renderUIURL(url));
  }

  void showScreenCam() { showUIURL("Screen Cam"); }
  void showWebCam() { showUIURL("Web Cam"); }

  JComponent challengesPanel() { 
    if (challengesPanel == null)
      challengesPanel = new G22ChallengesPanel().g22utils(g22utils);
    ret challengesPanel.visualize();
  }

  class GalleryPanel is Swingable {
    transient JGallery gallery;
    
    void selectImage(GalleryImage img) {
      if (img != null) gallery.selectFile(img.path);
    }
      
    cachedVisualize {
      var galleryImageSurface = stdImageSurface()/*.verbose(true)*/;
      gallery = new JGallery;
      gallery.horizontal(false);
      gallery.imageLoader().imageHeight(50);
      gallery.onAdaptingButton((button, file) -> {
        if (!devMode()) ret;
        componentPopupMenuItem(button, "Upload image...",
          rThread { uploadImageFileDialog(file) });
      });
        
      new AWTOnConceptChangesByClass(concepts, GalleryImage, gallery.visualize(), -> {
        print("Updating gallery");
        var l = sortFilesAlphaNumIC(map(list(concepts, GalleryImage), i -> i.path));
        gallery.setImageFiles(l);
        updateEnclosingTabTitleWithCount(gallery.visualize(), l(l));
      }).install();

      var analysisPanel = new G22AnalysisPanel;
      analysisPanel.editAnalyzer = l1 editAnalyzer;
      analysisPanel.g22utils(g22utils).imageSurface(galleryImageSurface);
      ownResource(analysisPanel);
      main onChange(analysisPanel, me());

      gallery.openImage = img -> {
        galleryImageSurface.loadImage(img);
        analysisPanel.setImage(img);
      };
      
      ret withBottomMargin(
        jvsplit(0.75,
          jhsplit(0.25,
            jCenteredSection("All Images", gallery.visualize()),
            jRaisedCenteredSection("Selected Image",
              withRightAlignedButtons(galleryImageSurface.visualize(),
                "Gallery CRUD" := r { showUIURL("Gallery CRUD") }
              )
            )
          ),
          analysisPanel.visualize()
        ));
    }
  }

  transient simplyCached GalleryPanel galleryPanel() { ret new GalleryPanel; }
  
  JComponent entitiesPanel() { 
    if (entityCRUD == null)
      entityCRUD = new SimpleCRUD_v2(concepts, Entity);
    ret wrapCRUD("Entities", entityCRUD);
  }
  
  // for scripts
  Gazelle22_ImageToRegions imageToRegions(BufferedImage inputImage, SnPSettings snpSettings) {
    ret new Gazelle22_ImageToRegions(functionTimings, inputImage, snpSettings);
  }

  JComponent databasesPanel() {
    ret new G22DatabasesPanel(g22utils).visualize();
  }

  transient simplyCached JComponent paintPanel() {
    if (paintToolGallery == null) {
      paintToolGallery = JGallery().horizontal(false);
      paintToolGallery.openImage = img -> paintTool.loadImageProtected(img);
      paintToolGallery.imageLoader().imageHeight(50);
      
      new AWTOnConceptChangesByClass(concepts, GalleryImage, paintToolGallery.visualize(), -> {
        var images = galleryImagesIn(conceptsDir(concepts));
        print("Paint tool images: " + l(images));
        paintToolGallery.setImageFiles(sortFilesAlphaNumIC(images));
      }).install();
    }
    
    if (paintTool == null) paintTool(new JPaintTool);
    ownResource(paintTool);
    main onChange(paintTool, me());

    paintTool.createAutoPersistFile =
      -> makeFileNameUnique_beforeExtension_startWith1_noDot(conceptsDir(concepts, "Painting.png"));
    
    new JG22Labels labelsView;
    paintTool.bottomLeftControls = -> labelsView.visualize();

    var paintToolVis = paintTool.visualize();

    paintTool.imageSurface().defaultImageDir = -> g22utils.dbDir();

    // migrating between directories - just make a new file
    if (!isDeepContainedInDir_canonical(paintTool.autoPersistFile(), conceptsDir(concepts)))
      paintTool.newImage();

    paintTool.varAutoPersistFile().onChangeAndNow(imageFile -> {
      labelsView.setLabels(g22utils.labelsForFile(imageFile));
      paintToolGallery.selectFile(imageFile);
    });
    labelsView.addLabel = text -> {
      var imageFile = paintTool.autoPersistFile();
      var labels = g22utils.labelsForFile(imageFile);
      labels.add(g22utils.getLabel(text));
      print(+labels);
      g22utils.setLabelsForFile(imageFile, labels);
      labelsView.setLabels(labels);
    };
    
    labelsView.removeLabel = label -> {
      var imageFile = paintTool.autoPersistFile();
      var labels = g22utils.labelsForFile(imageFile);
      labels = listWithout(labels, label);
      g22utils.setLabelsForFile(imageFile, labels);
      labelsView.setLabels(labels);
    };

    paintAnalysisPanel if null = new G22AnalysisPanel;
    paintAnalysisPanel.editAnalyzer = l1 editAnalyzer;
    paintAnalysisPanel.g22utils(g22utils).imageSurface(paintTool.imageSurface());
    ownResource(paintAnalysisPanel);
    main onChange(paintAnalysisPanel, me());

    paintTool.onImageChanged(img -> paintAnalysisPanel.setImage(img));
    paintAnalysisPanel.setImage(paintTool.getImage());

    var collapsiblePanel = CollapsibleLeftPanel(false, "Paintings",
      paintToolGallery.visualize(), paintToolVis);
    linkVars(varPaintToolGalleryExpanded(), collapsiblePanel.varExpanded());
    paintToolGallery.varFiles().onChangeAndNow(files -> collapsiblePanel.sidePanelName(n2(files, "Painting")));

    ret jvsplit(0.75,
      collapsiblePanel.visualize(),
      jRaisedCenteredSection("Image Analysis", paintAnalysisPanel.visualize()));
  }

  JComponent videoPanel() {
    if (videoLibDownloader == null)
      videoLibDownloader = new JVideoLibDownloader()
        .forward(-> videoPlayer().visualize());
    ret videoLibDownloader.visualize();
  }

  JFFMPEGVideoPlayer videoPlayer() {
    if (videoPlayer == null)
      videoPlayer = new JFFMPEGVideoPlayer;
    ret videoPlayer;
  }

  transient simplyCached G22AnalyzersPanel analyzersPanel() {
    ret new G22AnalyzersPanel(g22utils);
  }

  transient simplyCached G22ScriptsPanel scriptsPanel() {
    ret new G22ScriptsPanel(g22utils);
  }

  Cl<File> galleryImagesIn(File dir) {
    ret mapNonNulls(list(concepts, GalleryImage),
      i -> isDeepContainedInDir_absolute(i.path, dir) ? i.path : null);
  }

  void editAnalyzer(G22Recognizer analyzer) {
    if (analyzer == null) ret;
    analyzersPanel().edit(analyzer);
    showUIURL("Analyzers");
  }

  O inlineScript(long scriptID, VarContext varContext) {
    var script = getConcept(concepts, G22LeftArrowScript, scriptID);
    if (script == null) fail("Script ID " + scriptID + " not found");
    GazelleV_LeftArrowScript.Script parsedScript = script.compileSaved().parsedScript;
    ret parsedScript.get(varContext);
  }

  transient simplyCached StudyPanel studyPanel() { ret new StudyPanel; }

  SingleComponentPanel jOnDemand(IF0<JComponent> makeComponent) {
    ret main jOnDemand(-> { temp enter(); ret makeComponent?!; });
  }

  // implementation of G22ProjectActions

  public void editScripts() {
    showUIURL("Scripts");
  }

  public void editProjectStory() {
    showUIURL("Story");
  }

  public void openObjectInProject(long id) {
    var c = getConcept(concepts, id);
    if (c == null)
      infoBox("Object ID not found in project: " + id);
    else
      openConcept(c);
  }

  public void openConcept(Concept c) {
    if (c cast G22Analyzer) {
      showUIURL("Analyzers");
      analyzersPanel().setSelected(c);
    } else if (c cast G22LeftArrowScript) {
      showUIURL("Scripts");
      scriptsPanel().setSelected(c);
    } else if (c cast G22GalleryImage) {
      showInGallery(c);
    } else
      infoBox("Don't know how to show " + c);
  }
  
  public void openPathInProject(S path) {
    path = trim(path);
    
    if (isInteger(path))
      ret with openObjectInProject(parseLong(path));

    if (uiURLs.hasURL(path))
      ret with uiURLs.showUIURL(path);
      
    File f = newFile(dbDir(), path);
    print(+f);
    if (isFile(f)) {
      if (isImageFile(f)) {
        showInGallery(addToGallery(f));
      } else
        infoBox("Unknown file type: " + fileName(f));
    } else
      infoBox("Not a UI URL, object ID or project file: " + path);
  }

  void showInGallery(G22GalleryImage img) {
    if (img == null) ret;
    showUIURL("Gallery");
    galleryPanel().selectImage(img);
  }

  BufferedImage getGalleryImage(long id) {
    var img = getConcept(concepts, G22GalleryImage, id);
    if (img == null) fail("Image not found: " + id);
    ret img.load();
  }

  void cleanMeUp_g22utils {
    g22utils.close();
  }

  public void waitUntilStarted aka waitForAutoStart() {
    g22utils.autoStarter().waitUntilDone();
  }

  S functionsIncludeID() { ret #1034034; }

  void infoBoxAndCompile(S msg) {
    //topLeftInfoBox(msg);
    infoBox(msg);
    if (cic(msg, "edited"))
      dm_callOS("compile");
  }

  JComponent visualizeJavaObject(O o) {
    ret wrap(G22JavaObjectVisualizer(g22utils, o));
  }
  
  bool devMode() { ret haveMuricaPassword(); }
} // end of module

concept SavedRegion {
  new Ref image; // e.g. a GalleryImage
  SnPSettings snpSettings;
  int regionIndex; // region number relative to snpSettings
  //Rect bounds; // get it from bitMatrix instead
  ScanlineBitMatrix bitMatrix;
  //new RefL<Label> labels;
}

concept Example {
  new Ref<Label> label;
  new Ref item; // e.g. a SavedRegion
  double confidence = 1;
}

concept IfThenTheory {
  new Ref if_;
  new Ref then;
}

asclass WatchTarget {
  void mainWindow(Window window) {}
  abstract void configureScreenCamStream(ScreenCamStream stream);
}

// screenNr: 1 = screen 1 etc
srecord WatchScreen(int screenNr) > WatchTarget {
  toString { ret "Screen " + screenNr; }

  void configureScreenCamStream(ScreenCamStream stream) {
    stream.useScreen(screenNr-1);
  }
}

srecord WatchMouse(int width, int height) > WatchTarget {
  WatchMouse() {
    height = 256;
    width = iround(height*16.0/9);
  }
  
  toString { ret "Mouse"; }
  
  void configureScreenCamStream(ScreenCamStream stream) {
    stream.area(mouseArea(width, height));
  }
}

srecord WatchScreenWithMouse > WatchTarget {
  toString { ret "Screen w/mouse"; }

  void configureScreenCamStream(ScreenCamStream stream) {
    stream.useScreen(screenNrContaining(mouseLocationPt()));
  }
}

// Screen where Gazelle window is not
srecord WatchOtherScreen > WatchTarget {
  toString { ret "Other Screen"; }

  transient void settable Window mainWindow;

  void configureScreenCamStream(ScreenCamStream stream) {
    stream.useScreen(mod(screenNrOfWindow(mainWindow)+1, numberOfScreens()));
  }
}

!include once #1034593 // Entity etc.

Author comment

Began life as a copy of #1033862

download  show line numbers  debug dex  old transpilations   

Travelled to 2 computer(s): bhatertpkbcr, mqqgnosmbjvj

No comments. add comment

Snippet ID: #1035021
Snippet name: Gazelle Screen Cam / Gazelle 22 Module [backup]
Eternal ID of this version: #1035021/1
Text MD5: 6722f1afd4244dd76520dd9a4943d8b2
Author: stefan
Category: javax / gazelle v
Type: JavaX source code (Dynamic Module)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2022-03-23 01:33:04
Source code size: 46978 bytes / 1423 lines
Pitched / IR pitched: No / No
Views / Downloads: 120 / 126
Referenced in: [show references]