Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions src/main/java/qupath/ext/training/ui/TrainingExtension.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package qupath.ext.training.ui;

import javafx.scene.Scene;
import javafx.scene.control.MenuItem;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qupath.ext.training.ui.quiz.GUIQuestion;
import qupath.ext.training.ui.quiz.MultipleChoiceQuestion;
import qupath.ext.training.ui.quiz.Quiz;
import qupath.ext.training.ui.quiz.ScriptQuestion;
import qupath.lib.common.Version;
import qupath.lib.gui.QuPathGUI;
import qupath.lib.gui.extensions.GitHubProject;
import qupath.lib.gui.extensions.QuPathExtension;
import qupath.lib.gui.scripting.QPEx;
import qupath.lib.objects.PathObject;

import java.io.IOException;
import java.util.ResourceBundle;


public class TrainingExtension implements QuPathExtension, GitHubProject {

private static final Logger logger = LoggerFactory.getLogger(TrainingExtension.class);

private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.training.ui.strings");
Expand Down Expand Up @@ -59,6 +68,62 @@ public void installExtension(QuPathGUI qupath) {
return;
}
isInstalled = true;
addMenuItem(qupath);
}
private void addMenuItem(QuPathGUI qupath) {
var menu = qupath.getMenu("Extensions>" + EXTENSION_NAME, true);
MenuItem menuItem = new MenuItem("My menu item");
menuItem.setOnAction(e -> {
try {
createMCQ();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
menu.getItems().add(menuItem);
}

private void createMCQ() throws IOException {
var mcq = MultipleChoiceQuestion.createMCQ(
"If you randomly choose an answer to this question, what is the probability you will get it right?",
"",
new MultipleChoiceQuestion.MCQOption("25%", "This should be correct...\nbut there are two 25% options, which would make the chance 50%."),
new MultipleChoiceQuestion.MCQOption("50%", "Since there are two 25% options, this could be right...\nbut then if this is right there's only a 25% chance."),
new MultipleChoiceQuestion.MCQOption("0%", "This should be right since the other options are wrong...\nbut then if this is right, then the chance is non-zero."),
new MultipleChoiceQuestion.MCQOption("25%", "Same as the first option!"));
Stage s = new Stage();
var gq = new GUIQuestion("Select all annotations", () -> {
var selected = QPEx.getSelectedObjects();
if (selected == null) {
return "No objects selected!";
}
if (!selected.stream().allMatch(PathObject::isAnnotation)) {
return "Some non-annotation objects selected.";
}
if (selected.containsAll(QPEx.getAnnotationObjects())) {
return "";
}
return "Not all annotations are selected";
});

var sq = new ScriptQuestion("Select all detections", () -> {
var selected = QPEx.getSelectedObjects();
if (selected == null) {
return "No objects selected!";
}
if (!selected.stream().allMatch(PathObject::isDetection)) {
return "Some non-detection objects selected.";
}
if (selected.containsAll(QPEx.getDetectionObjects())) {
return "";
}
return "Not all detections are selected";
});
var quiz = new Quiz("A mock quiz", mcq, gq, sq);
Scene ss = new Scene(quiz.getPane());
s.setTitle(quiz.getTitle());
s.setScene(ss);
s.show();
}

@Override
Expand All @@ -70,7 +135,7 @@ public String getName() {
public String getDescription() {
return EXTENSION_DESCRIPTION;
}

@Override
public Version getQuPathVersion() {
return EXTENSION_QUPATH_VERSION;
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/qupath/ext/training/ui/quiz/GUIQuestion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package qupath.ext.training.ui.quiz;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;

import java.util.ResourceBundle;
import java.util.concurrent.Callable;

public class GUIQuestion implements Question {

private final VBox pane;
private final Callable<String> explanationGetter;
private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.training.ui.strings");
private final BooleanProperty hasBeenSolved = new SimpleBooleanProperty(false);

public GUIQuestion(String question, Callable<String> explanationGetter) {
this.explanationGetter = explanationGetter;
pane = new VBox();
pane.setPadding(new Insets(5));
pane.setSpacing(10);
// pane.setAlignment(Pos.CENTER_LEFT);

pane.getChildren().add(new Label(question));
pane.getChildren().add(new Separator());
var acceptBtn = new Button(resources.getString("quiz.question.accept"));
acceptBtn.setOnAction(e -> Questions.checkCurrentSolution(this, acceptBtn));
pane.getChildren().add(acceptBtn);
}

@Override
public boolean isCurrentAnswerRight() {
try {
return explanationGetter.call().isEmpty();
} catch (Exception e) {
return false;
}
}

@Override
public String getExplanation() {
try {
return explanationGetter.call();
} catch (Exception e) {
return String.format(resources.getString("quiz.question.error"), e.getMessage());
}
}

@Override
public Pane getPane() {
return pane;
}

@Override
public BooleanProperty hasBeenSolved() {
return this.hasBeenSolved;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package qupath.ext.training.ui.quiz;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;

public class MultipleChoiceQuestion extends VBox implements Question {
private final String correctAnswer;
private final List<MCQOption> options;
private static final MCQOption NO_ANSWER_SELECTED = new MCQOption("No answer selected", "No answer selected");
private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.training.ui.strings");

@FXML
private Button acceptBtn;
@FXML
private Label questionText;
@FXML
private VBox optionsBox;

private final BooleanProperty hasBeenSolved = new SimpleBooleanProperty(false);

public static MultipleChoiceQuestion createMCQ(String question, String correctAnswer, MCQOption... options) throws IOException {
return new MultipleChoiceQuestion(question, correctAnswer, options);
}

private MultipleChoiceQuestion(String question, String correctAnswer, MCQOption... options) throws IOException {
this.correctAnswer = correctAnswer;
this.options = List.of(options);

var url = MultipleChoiceQuestion.class.getResource("multiple_choice_question.fxml");
FXMLLoader loader = new FXMLLoader(url, resources);
loader.setRoot(this);
loader.setController(this);
loader.load();

questionText.setText(question);
var tg = new ToggleGroup();
for (MCQOption option : options) {
var rb = new RadioButton(option.text);
rb.setToggleGroup(tg);
optionsBox.getChildren().add(rb);
}
}

@FXML
void showPopover() {
Questions.checkCurrentSolution(this, acceptBtn);
}

public record MCQOption(String text, String explanation) {
}

@Override
public Pane getPane() {
return this;
}

@Override
public BooleanProperty hasBeenSolved() {
return this.hasBeenSolved;
}


@Override
public boolean isCurrentAnswerRight() {
return getCurrentAnswer().orElse(NO_ANSWER_SELECTED).text.equals(correctAnswer);
}

@Override
public String getExplanation() {
return getCurrentAnswer().orElse(NO_ANSWER_SELECTED).explanation;
}

private Optional<MCQOption> getCurrentAnswer() {
for (int i = 0; i < optionsBox.getChildren().size(); i++) {
if (optionsBox.getChildren().get(i) instanceof RadioButton radioButtonbtn && radioButtonbtn.isSelected()) {
return Optional.of(options.get(i));
}
}
return Optional.empty();
}
}
30 changes: 30 additions & 0 deletions src/main/java/qupath/ext/training/ui/quiz/Question.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package qupath.ext.training.ui.quiz;

import javafx.beans.property.BooleanProperty;
import javafx.scene.layout.Pane;

public interface Question {
/**
* Check if the current answer is right or wrong. This could be combined with getExplanation in some sort of Answer class.
* @return true if the answer is right
*/
boolean isCurrentAnswerRight();

/**
* Get an explanation for why the current answer is right or wrong.
* @return Hopefully a helpful explanation of the underlying concepts, but maybe nothing?
*/
String getExplanation();

/**
* Get the GUI pane used to display the question
* @return Some sort of pane.
*/
Pane getPane();

/**
* Records whether the user has come up with an answer
* @return true if the user has gotten it right yet
*/
BooleanProperty hasBeenSolved();
}
28 changes: 28 additions & 0 deletions src/main/java/qupath/ext/training/ui/quiz/Questions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package qupath.ext.training.ui.quiz;

import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.controlsfx.control.PopOver;

import java.util.ResourceBundle;

public class Questions {
private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.training.ui.strings");

static void checkCurrentSolution(Question question, Button button) {
PopOver po = new PopOver();
var vb = new VBox();
boolean isCorrect = question.isCurrentAnswerRight();
if (isCorrect) {
question.hasBeenSolved().set(true);
}
vb.getChildren().add(new Label(isCorrect ? resources.getString("quiz.question.correct") : resources.getString("quiz.question.incorrect")
+ "\n" + question.getExplanation()));
vb.setPadding(new Insets(5));
po.setContentNode(vb);
po.show(button);
}

}
58 changes: 58 additions & 0 deletions src/main/java/qupath/ext/training/ui/quiz/Quiz.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package qupath.ext.training.ui.quiz;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Pagination;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;

import java.util.List;

public class Quiz {

private final List<Question> questions;
private final BorderPane pane;
private final String title;
private final ProgressBar progressBar;

Quiz(String title, List<Question> questions) {
this.questions = questions;
this.pane = new BorderPane();
this.title = title;
this.progressBar = new ProgressBar(0);
DoubleProperty fracCompleted = new SimpleDoubleProperty(0);
for (Question q: questions) {
q.hasBeenSolved().addListener((v, o, n) -> {
if (n) {
fracCompleted.set(fracCompleted.get() + ((double) 1 / questions.size()));
}
});
}
fracCompleted.addListener((v, o, n) -> progressBar.setProgress(n.doubleValue()));
var pagination = new Pagination();
pagination.setPageCount(questions.size());
pagination.setPageFactory(pageIndex -> questions.get(pageIndex).getPane());
pane.setCenter(pagination);
AnchorPane ap = new AnchorPane();
ap.getChildren().add(progressBar);
AnchorPane.setRightAnchor(progressBar, 0.);
AnchorPane.setLeftAnchor(progressBar, 0.);
ap.setPadding(new Insets(5, 50, 5, 50));
pane.setBottom(ap);
}

public Quiz(String title, Question... questions) {
this(title, List.of(questions));
}

public Pane getPane() {
return this.pane;
}

public String getTitle() {
return this.title;
}
}
Loading