Skip to content

Commit 69f5704

Browse files
authored
Make previews always fit vertically in the preview area (#944)
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
1 parent 11bdb8e commit 69f5704

12 files changed

+191
-79
lines changed

ui/src/main/java/edu/wpi/grip/ui/preview/BlobsSocketPreviewView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ protected void convertImage() {
8383
final Mat output = tmp;
8484
final int numBlobs = blobsReport.getBlobs().size();
8585
platform.runAsSoonAsPossible(() -> {
86-
final Image image = this.imageConverter.convert(output, getImageHeight());
86+
final Image image = this.imageConverter.convert(output);
8787
this.imageView.setImage(image);
8888
this.infoLabel.setText("Found " + numBlobs + " blobs");
8989
});

ui/src/main/java/edu/wpi/grip/ui/preview/ContoursSocketPreviewView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ protected void convertImage() {
7777
final long finalNumContours = numContours;
7878
final Mat convertInput = tmp;
7979
platform.runAsSoonAsPossible(() -> {
80-
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
80+
final Image image = this.imageConverter.convert(convertInput);
8181
this.imageView.setImage(image);
8282
this.infoLabel.setText("Found " + finalNumContours + " contours");
8383
});

ui/src/main/java/edu/wpi/grip/ui/preview/ImageBasedPreviewView.java

+1-19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.google.common.eventbus.Subscribe;
99

1010
import javafx.application.Platform;
11-
import javafx.scene.image.ImageView;
1211

1312
import static org.bytedeco.javacpp.opencv_core.CV_8S;
1413
import static org.bytedeco.javacpp.opencv_core.CV_8U;
@@ -26,9 +25,7 @@ public abstract class ImageBasedPreviewView<T> extends SocketPreviewView<T> {
2625
/**
2726
* The view showing the image.
2827
*/
29-
protected final ImageView imageView = new ImageView();
30-
31-
private int imageHeight = 1;
28+
protected final ResizableImageView imageView = new ResizableImageView();
3229

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

42-
/**
43-
* Gets the height of the image to render.
44-
*/
45-
protected final int getImageHeight() {
46-
return imageHeight;
47-
}
48-
4939
/**
5040
* Converts the input data to an image and render it in the {@link #imageView}.
5141
*/
@@ -71,12 +61,4 @@ public final void onRenderEvent(RenderEvent e) {
7161
convertImage();
7262
}
7363

74-
/**
75-
* Resizes the image based on the given height while preserving the ratio.
76-
*/
77-
public final void resize(int imageHeight) {
78-
this.imageHeight = imageHeight;
79-
convertImage();
80-
}
81-
8264
}

ui/src/main/java/edu/wpi/grip/ui/preview/ImageSocketPreviewView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected void convertImage() {
2929
.filter(ImageBasedPreviewView::isPreviewable)
3030
.ifPresent(m -> {
3131
platform.runAsSoonAsPossible(() -> {
32-
Image image = imageConverter.convert(m.getCpu(), getImageHeight());
32+
Image image = imageConverter.convert(m.getCpu());
3333
imageView.setImage(image);
3434
});
3535
});

ui/src/main/java/edu/wpi/grip/ui/preview/LinesSocketPreviewView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ protected void convertImage() {
100100
final Mat convertInput = input;
101101
final int numLines = lines.size();
102102
platform.runAsSoonAsPossible(() -> {
103-
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
103+
final Image image = this.imageConverter.convert(convertInput);
104104
this.imageView.setImage(image);
105105
this.infoLabel.setText("Found " + numLines + " lines");
106106
});

ui/src/main/java/edu/wpi/grip/ui/preview/PreviewsController.java

+1-14
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import javafx.collections.FXCollections;
1919
import javafx.collections.ObservableList;
2020
import javafx.fxml.FXML;
21-
import javafx.scene.control.ScrollPane;
2221
import javafx.scene.layout.HBox;
2322

2423
import javax.inject.Inject;
@@ -33,8 +32,6 @@
3332
@Singleton
3433
public class PreviewsController {
3534

36-
@FXML
37-
private ScrollPane scrollPane;
3835
@FXML
3936
private HBox previewBox;
4037

@@ -45,11 +42,8 @@ public class PreviewsController {
4542
@Inject
4643
private SocketPreviewViewFactory previewViewFactory;
4744

48-
private static final int PREVIEW_PADDING = 50;
49-
5045
@FXML
5146
private void initialize() {
52-
scrollPane.heightProperty().addListener((obs, o, n) -> resizePreviews(n.intValue()));
5347
}
5448

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

178-
private void resizePreviews(int height) {
179-
getPreviews().stream()
180-
.filter(p -> p instanceof ImageBasedPreviewView)
181-
.map(p -> (ImageBasedPreviewView) p)
182-
.forEach(p -> p.resize(height - PREVIEW_PADDING));
183-
}
184-
185172
}

ui/src/main/java/edu/wpi/grip/ui/preview/RectangleSocketPreviewView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ protected void convertImage() {
7878
final Mat convertInput = tmp;
7979
final int numRegions = rectangles.size();
8080
platform.runAsSoonAsPossible(() -> {
81-
final Image image = this.imageConverter.convert(convertInput, getImageHeight());
81+
final Image image = this.imageConverter.convert(convertInput);
8282
this.imageView.setImage(image);
8383
this.infoLabel.setText("Found " + numRegions + " regions of interest");
8484
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package edu.wpi.grip.ui.preview;
2+
3+
import javafx.beans.property.DoubleProperty;
4+
import javafx.beans.property.ObjectProperty;
5+
import javafx.beans.property.ReadOnlyDoubleProperty;
6+
import javafx.beans.property.SimpleDoubleProperty;
7+
import javafx.beans.property.SimpleObjectProperty;
8+
import javafx.geometry.Orientation;
9+
import javafx.scene.image.Image;
10+
import javafx.scene.layout.Background;
11+
import javafx.scene.layout.BackgroundImage;
12+
import javafx.scene.layout.BackgroundRepeat;
13+
import javafx.scene.layout.BackgroundSize;
14+
import javafx.scene.layout.Region;
15+
16+
/**
17+
* A custom implementation of an image view that resizes to fit its parent container.
18+
*/
19+
public class ResizableImageView extends Region {
20+
21+
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
22+
private final DoubleProperty ratio = new SimpleDoubleProperty(this, "ratio", 1);
23+
private static final BackgroundSize size =
24+
new BackgroundSize(BackgroundSize.AUTO, BackgroundSize.AUTO, false, false, true, false);
25+
26+
/**
27+
* Creates a new resizable image view.
28+
*/
29+
public ResizableImageView() {
30+
super();
31+
32+
getStyleClass().add("resizable-image");
33+
34+
image.addListener((obs, old, img) -> {
35+
if (img == null) {
36+
setBackground(null);
37+
ratio.set(1);
38+
} else if (img != old) {
39+
// Only create a new background object when the image changes
40+
// Otherwise we would be creating a new background object for every frame of every preview
41+
Background background = createImageBackground(img);
42+
setBackground(background);
43+
ratio.set(img.getWidth() / img.getHeight());
44+
setPrefHeight(img.getHeight());
45+
setPrefWidth(USE_COMPUTED_SIZE);
46+
}
47+
});
48+
}
49+
50+
/**
51+
* Creates a background that displays only the given image.
52+
*
53+
* @param img the image to create the background for
54+
*/
55+
private static Background createImageBackground(Image img) {
56+
BackgroundImage backgroundImage = new BackgroundImage(
57+
img,
58+
BackgroundRepeat.NO_REPEAT,
59+
BackgroundRepeat.NO_REPEAT,
60+
null,
61+
size
62+
);
63+
return new Background(backgroundImage);
64+
}
65+
66+
public Image getImage() {
67+
return image.get();
68+
}
69+
70+
public ObjectProperty<Image> imageProperty() {
71+
return image;
72+
}
73+
74+
public void setImage(Image image) {
75+
this.image.set(image);
76+
}
77+
78+
public double getRatio() {
79+
return ratio.get();
80+
}
81+
82+
public ReadOnlyDoubleProperty ratioProperty() {
83+
return ratio;
84+
}
85+
86+
@Override
87+
public Orientation getContentBias() {
88+
return Orientation.VERTICAL;
89+
}
90+
91+
/**
92+
* Computes the width of the displayed image for the given target height, maintaining the image's
93+
* intrinsic aspect ratio.
94+
*
95+
* @param height the target height of the image
96+
* @return the width of the image
97+
*/
98+
private double computeImageWidthForHeight(double height) {
99+
if (getImage() == null) {
100+
return 1;
101+
}
102+
return height * getRatio();
103+
}
104+
105+
@Override
106+
protected double computePrefWidth(double height) {
107+
return computeImageWidthForHeight(height);
108+
}
109+
}

ui/src/main/java/edu/wpi/grip/ui/preview/SocketPreviewView.java

-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

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

5-
import javafx.scene.control.TitledPane;
6-
75
import static com.google.common.base.Preconditions.checkNotNull;
86

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

2220
this.setText(this.getTitle());
2321
this.getStyleClass().add("socket-preview");
24-
this.setCollapsible(false);
2522
}
2623

2724
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package edu.wpi.grip.ui.preview;
2+
3+
import javafx.beans.property.ObjectProperty;
4+
import javafx.beans.property.SimpleObjectProperty;
5+
import javafx.beans.property.StringProperty;
6+
import javafx.scene.Node;
7+
import javafx.scene.control.Label;
8+
import javafx.scene.layout.BorderPane;
9+
import javafx.scene.layout.HBox;
10+
import javafx.scene.layout.StackPane;
11+
12+
/**
13+
* Custom implementation of a titled pane. The JavaFX implementation has a tendency to add a gap
14+
* on the sides of image content when resized.
15+
*/
16+
public class TitledPane extends BorderPane {
17+
18+
private final Label label = new Label();
19+
private final HBox top = new HBox(label);
20+
private final StackPane center = new StackPane();
21+
22+
private final ObjectProperty<Node> content = new SimpleObjectProperty<>();
23+
24+
/**
25+
* Creates a new titled pane with no text and no content.
26+
*/
27+
public TitledPane() {
28+
this.getStyleClass().add("titled-pane");
29+
top.getStyleClass().add("title");
30+
center.getStyleClass().add("content");
31+
32+
content.addListener((obs, old, content) -> {
33+
if (content == null) {
34+
center.getChildren().clear();
35+
} else {
36+
center.getChildren().setAll(content);
37+
}
38+
});
39+
40+
setTop(top);
41+
setCenter(center);
42+
setMaxHeight(USE_PREF_SIZE);
43+
}
44+
45+
public void setText(String text) {
46+
label.setText(text);
47+
}
48+
49+
public String getText() {
50+
return label.getText();
51+
}
52+
53+
public StringProperty textProperty() {
54+
return label.textProperty();
55+
}
56+
57+
public Node getContent() {
58+
return content.get();
59+
}
60+
61+
public ObjectProperty<Node> contentProperty() {
62+
return content;
63+
}
64+
65+
public void setContent(Node content) {
66+
this.content.set(content);
67+
}
68+
}

0 commit comments

Comments
 (0)