sclass ButtonImageLoader {
  DynamicStack stack;
  Set<JButton> loadedButtons = weakIdentityHashSet();
  DeQ loadQ;
  JScrollPane scrollPane;
  ChangeListener changeListener;
  settable int imageHeight = 128;

  *(DynamicStack *stack) {
    bindToComponent(stack, -> {
      scrollPane = enclosingScrollPane(stack);
      changeListener = scrollPaneOnScroll(scrollPane, r { update() });
      update();
    }, -> {
      if (scrollPane != null) {
        scrollPane.getViewport().removeChangeListener(changeListener);
        scrollPane = null;
        changeListener = null;
      }
    });
  }
  
  void update() swing {
    //new L<Runnable> l;
    for (JButton b : reversed((L<JButton>) (L) stackElementsShowing(stack))) {
      //if (add(loadedButtons, b)) pcall { // will strangely not work
      if (add_byContains(loadedButtons, b)) pcall {
        TextImageAction tia = cast metaGet(b, TextImageAction.class);
        if (tia == null) continue with print("no tia");
        File f = tia.file;
        if (f == null) continue with print("no file");
        qAdd(r { setAButtonsImage(b, f) });
      }
    }
    //if (nempty(l)) qAdd(chainRunnables_pcall(l));
  }
  
  void qAdd(Runnable r) {
    if (loadQ == null) loadQ = startDeQ();
    loadQ.addFirst(r);
  }
  
  // overridable
  void setAButtonsImage(JButton b, File f) {
    setButtonImage(b, loadPreviewImage(f));
    //print("Loaded: " + f);
  }
  
  // overridable
  BufferedImage loadPreviewImage(File f) {
    ret loadPreviewImageWithExactHeight(imageHeight, f);
  }
}