Skip to content

Commit

Permalink
Make previews always fit vertically in the preview area (#944)
Browse files Browse the repository at this point in the history
No more previews clipping below the bottom, forcing users to scroll up and down to see information displayed below the image (eg contour count)

Reimplement TitledPane and ImageView to get proper behavior
  • Loading branch information
SamCarlberg authored May 16, 2019
1 parent 11bdb8e commit 69f5704
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ protected void convertImage() {
final Mat output = tmp;
final int numBlobs = blobsReport.getBlobs().size();
platform.runAsSoonAsPossible(() -> {
final Image image = this.imageConverter.convert(output, getImageHeight());
final Image image = this.imageConverter.convert(output);
this.imageView.setImage(image);
this.infoLabel.setText("Found " + numBlobs + " blobs");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected void convertImage() {
final long finalNumContours = numContours;
final Mat convertInput = tmp;
platform.runAsSoonAsPossible(() -> {
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
final Image image = this.imageConverter.convert(convertInput);
this.imageView.setImage(image);
this.infoLabel.setText("Found " + finalNumContours + " contours");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.google.common.eventbus.Subscribe;

import javafx.application.Platform;
import javafx.scene.image.ImageView;

import static org.bytedeco.javacpp.opencv_core.CV_8S;
import static org.bytedeco.javacpp.opencv_core.CV_8U;
Expand All @@ -26,9 +25,7 @@ public abstract class ImageBasedPreviewView<T> extends SocketPreviewView<T> {
/**
* The view showing the image.
*/
protected final ImageView imageView = new ImageView();

private int imageHeight = 1;
protected final ResizableImageView imageView = new ResizableImageView();

/**
* @param socket An output socket to preview.
Expand All @@ -39,13 +36,6 @@ protected ImageBasedPreviewView(OutputSocket<T> socket) {
+ " exposing constructor to another thread!";
}

/**
* Gets the height of the image to render.
*/
protected final int getImageHeight() {
return imageHeight;
}

/**
* Converts the input data to an image and render it in the {@link #imageView}.
*/
Expand All @@ -71,12 +61,4 @@ public final void onRenderEvent(RenderEvent e) {
convertImage();
}

/**
* Resizes the image based on the given height while preserving the ratio.
*/
public final void resize(int imageHeight) {
this.imageHeight = imageHeight;
convertImage();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected void convertImage() {
.filter(ImageBasedPreviewView::isPreviewable)
.ifPresent(m -> {
platform.runAsSoonAsPossible(() -> {
Image image = imageConverter.convert(m.getCpu(), getImageHeight());
Image image = imageConverter.convert(m.getCpu());
imageView.setImage(image);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ protected void convertImage() {
final Mat convertInput = input;
final int numLines = lines.size();
platform.runAsSoonAsPossible(() -> {
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
final Image image = this.imageConverter.convert(convertInput);
this.imageView.setImage(image);
this.infoLabel.setText("Found " + numLines + " lines");
});
Expand Down
15 changes: 1 addition & 14 deletions ui/src/main/java/edu/wpi/grip/ui/preview/PreviewsController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;

import javax.inject.Inject;
Expand All @@ -33,8 +32,6 @@
@Singleton
public class PreviewsController {

@FXML
private ScrollPane scrollPane;
@FXML
private HBox previewBox;

Expand All @@ -45,11 +42,8 @@ public class PreviewsController {
@Inject
private SocketPreviewViewFactory previewViewFactory;

private static final int PREVIEW_PADDING = 50;

@FXML
private void initialize() {
scrollPane.heightProperty().addListener((obs, o, n) -> resizePreviews(n.intValue()));
}

/**
Expand Down Expand Up @@ -131,7 +125,7 @@ public synchronized void onSocketPreviewChanged(SocketPreviewChangedEvent event)
// When a socket previewed, add a new view, then sort all of the views so they stay ordered
SocketPreviewView<?> view = previewViewFactory.create(socket);
if (view instanceof ImageBasedPreviewView) {
((ImageBasedPreviewView) view).resize((int) (previewBox.getHeight()) - PREVIEW_PADDING);
((ImageBasedPreviewView) view).convertImage();
}
previews.add(view);
sortPreviews(previews);
Expand Down Expand Up @@ -175,11 +169,4 @@ private void sortPreviews(ObservableList<SocketPreviewView<?>> previews) {
FXCollections.sort(previews, comparePreviews);
}

private void resizePreviews(int height) {
getPreviews().stream()
.filter(p -> p instanceof ImageBasedPreviewView)
.map(p -> (ImageBasedPreviewView) p)
.forEach(p -> p.resize(height - PREVIEW_PADDING));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ protected void convertImage() {
final Mat convertInput = tmp;
final int numRegions = rectangles.size();
platform.runAsSoonAsPossible(() -> {
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
final Image image = this.imageConverter.convert(convertInput);
this.imageView.setImage(image);
this.infoLabel.setText("Found " + numRegions + " regions of interest");
});
Expand Down
109 changes: 109 additions & 0 deletions ui/src/main/java/edu/wpi/grip/ui/preview/ResizableImageView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package edu.wpi.grip.ui.preview;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Orientation;
import javafx.scene.image.Image;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Region;

/**
* A custom implementation of an image view that resizes to fit its parent container.
*/
public class ResizableImageView extends Region {

private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final DoubleProperty ratio = new SimpleDoubleProperty(this, "ratio", 1);
private static final BackgroundSize size =
new BackgroundSize(BackgroundSize.AUTO, BackgroundSize.AUTO, false, false, true, false);

/**
* Creates a new resizable image view.
*/
public ResizableImageView() {
super();

getStyleClass().add("resizable-image");

image.addListener((obs, old, img) -> {
if (img == null) {
setBackground(null);
ratio.set(1);
} else if (img != old) {
// Only create a new background object when the image changes
// Otherwise we would be creating a new background object for every frame of every preview
Background background = createImageBackground(img);
setBackground(background);
ratio.set(img.getWidth() / img.getHeight());
setPrefHeight(img.getHeight());
setPrefWidth(USE_COMPUTED_SIZE);
}
});
}

/**
* Creates a background that displays only the given image.
*
* @param img the image to create the background for
*/
private static Background createImageBackground(Image img) {
BackgroundImage backgroundImage = new BackgroundImage(
img,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
null,
size
);
return new Background(backgroundImage);
}

public Image getImage() {
return image.get();
}

public ObjectProperty<Image> imageProperty() {
return image;
}

public void setImage(Image image) {
this.image.set(image);
}

public double getRatio() {
return ratio.get();
}

public ReadOnlyDoubleProperty ratioProperty() {
return ratio;
}

@Override
public Orientation getContentBias() {
return Orientation.VERTICAL;
}

/**
* Computes the width of the displayed image for the given target height, maintaining the image's
* intrinsic aspect ratio.
*
* @param height the target height of the image
* @return the width of the image
*/
private double computeImageWidthForHeight(double height) {
if (getImage() == null) {
return 1;
}
return height * getRatio();
}

@Override
protected double computePrefWidth(double height) {
return computeImageWidthForHeight(height);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import edu.wpi.grip.core.sockets.OutputSocket;

import javafx.scene.control.TitledPane;

import static com.google.common.base.Preconditions.checkNotNull;

/**
Expand All @@ -21,7 +19,6 @@ protected SocketPreviewView(OutputSocket<T> socket) {

this.setText(this.getTitle());
this.getStyleClass().add("socket-preview");
this.setCollapsible(false);
}

/**
Expand Down
68 changes: 68 additions & 0 deletions ui/src/main/java/edu/wpi/grip/ui/preview/TitledPane.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package edu.wpi.grip.ui.preview;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;

/**
* Custom implementation of a titled pane. The JavaFX implementation has a tendency to add a gap
* on the sides of image content when resized.
*/
public class TitledPane extends BorderPane {

private final Label label = new Label();
private final HBox top = new HBox(label);
private final StackPane center = new StackPane();

private final ObjectProperty<Node> content = new SimpleObjectProperty<>();

/**
* Creates a new titled pane with no text and no content.
*/
public TitledPane() {
this.getStyleClass().add("titled-pane");
top.getStyleClass().add("title");
center.getStyleClass().add("content");

content.addListener((obs, old, content) -> {
if (content == null) {
center.getChildren().clear();
} else {
center.getChildren().setAll(content);
}
});

setTop(top);
setCenter(center);
setMaxHeight(USE_PREF_SIZE);
}

public void setText(String text) {
label.setText(text);
}

public String getText() {
return label.getText();
}

public StringProperty textProperty() {
return label.textProperty();
}

public Node getContent() {
return content.get();
}

public ObjectProperty<Node> contentProperty() {
return content;
}

public void setContent(Node content) {
this.content.set(content);
}
}
Loading

0 comments on commit 69f5704

Please sign in to comment.