ソースを参照

Add class selection and skill assignment to the LevelUpDialog

Sam Jaffe 8 年 前
コミット
f3ffcb494f

+ 2 - 1
resources/classes/Bard.json

@@ -43,6 +43,7 @@
     "Use Magic Device"
   ],
   "spells":{
+    "spellBookTypeName":".impl.Spontaneous",
     "group":"ARCANE",
     "ability":"CHA",
     "known":[
@@ -57,4 +58,4 @@
       ["Know Direction"]
     ]
   }
-}
+}

+ 1 - 1
resources/classes/Cleric.json

@@ -26,9 +26,9 @@
     "Spellcraft"
   ],
   "spells":{
+    "spellBookTypeName":"{\"@c\":\".impl.Inspired\",\"classRef\":\"Cleric\",\"spellInfo\":{}}",
     "group":"DIVINE",
     "ability":"WIS",
-    "known":[ ],
     "perDay":[
       [3, 1],
       [4, 2],

+ 70 - 0
resources/classes/Druid.json

@@ -0,0 +1,70 @@
+{
+  "name":"Druid",
+  "bab":"AVERAGE",
+  "fort":"GOOD",
+  "ref":"POOR",
+  "will":"GOOD",
+  "features":[
+    [
+      {
+        "@c":".impl.Simple",
+        "name":"Animal Companion"
+      },
+      {
+        "@c":".impl.Simple",
+        "name":"Nature Sense"
+      },
+      {
+        "@c":".impl.Simple",
+        "name":"Wild Empathy"
+      }
+    ]
+  ],
+  "skillPoints":4,
+  "skills":[
+    "Concentration",
+    "Craft (*)",
+    "Diplomacy",
+    "Handle Animal",
+    "Heal",
+    "Knowledge (nature)",
+    "Listen",
+    "Profession (*)",
+    "Ride",
+    "Spellcraft",
+    "Spot",
+    "Survival",
+    "Swim"
+  ],
+  "spells":{
+    "spellBookTypeName":"{\"@c\":\".impl.Inspired\",\"classRef\":\"Druid\",\"spellInfo\":{}}",
+    "group":"DIVINE",
+    "ability":"WIS",
+    "perDay":[
+      [3, 1],
+      [4, 2],
+      [4, 2, 1],
+      [5, 3, 2],
+      [5, 3, 2, 1],
+      [5, 3, 3, 2],
+      [6, 4, 3, 2, 1],
+      [6, 4, 3, 3, 2],
+      [6, 4, 4, 3, 2, 1],
+      [6, 4, 4, 3, 3, 2],
+      [6, 5, 4, 4, 3, 2, 1],
+      [6, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 5, 4, 4, 4, 3, 2],
+      [6, 5, 5, 5, 5, 4, 4, 3, 2, 1],
+      [6, 5, 5, 5, 5, 4, 4, 3, 3, 2],
+      [6, 5, 5, 5, 5, 5, 4, 4, 3, 3],
+      [6, 5, 5, 5, 5, 5, 4, 4, 4, 4]
+    ],
+    "spellList":[
+      ["Create Water"],
+      ["Cure Light Wounds"]
+    ]
+  }
+}

+ 16 - 4
src/main/lombok/org/leumasjaffe/charsheet/model/DDCharacterClass.java

@@ -12,18 +12,20 @@ import org.leumasjaffe.charsheet.model.observable.IntValue;
 import org.leumasjaffe.observer.Observable;
 
 import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
+import lombok.SneakyThrows;
 import lombok.experimental.Delegate;
 import lombok.experimental.FieldDefaults;
 
-@Data
+@Data @AllArgsConstructor
 @EqualsAndHashCode(callSuper=false)
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class DDCharacterClass extends Observable implements Comparable<DDCharacterClass> {
+public class DDCharacterClass extends Observable implements Comparable<DDCharacterClass>, Cloneable {
 	private static final class Reference {
-		DDClass base;
+		@Getter DDClass base;
 		
 		public boolean equals(Object o) {
 			return this == o || base.equals(o);
@@ -41,7 +43,6 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 			return base.toString();
 		}
 
-		@SuppressWarnings("unused")
 		public Reference(final String name) {
 			this.base = DDClass.getFromResource(name);
 		}
@@ -53,6 +54,12 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 	
 	Optional<DDSpellbook> spellBook;
 	
+	public DDCharacterClass(String name) {
+		this.level = new IntValue(0);
+		this.name = new Reference(name);
+		this.spellBook = getBase().createNewSpellBook();
+	}
+	
 	public String toString() {
 		return getName() + " " + getLevel();
 	}
@@ -104,4 +111,9 @@ public class DDCharacterClass extends Observable implements Comparable<DDCharact
 				.reduce(Stream.empty(), (l, r) -> Stream.concat(l, r))
 				.filter(p -> p.appliesTo(appliesScope)).collect(Collectors.toList());
 	}
+
+	@Override @SneakyThrows(CloneNotSupportedException.class)
+	public DDCharacterClass clone() {
+		return (DDCharacterClass) super.clone();
+	}
 }

+ 25 - 3
src/main/lombok/org/leumasjaffe/charsheet/model/DDClass.java

@@ -1,6 +1,7 @@
 package org.leumasjaffe.charsheet.model;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -13,8 +14,12 @@ import org.leumasjaffe.charsheet.model.features.DDProperty;
 import org.leumasjaffe.charsheet.model.magic.DDSpell;
 import org.leumasjaffe.charsheet.model.magic.DDSpellList;
 import org.leumasjaffe.charsheet.model.magic.DDSpellList.SpellList;
+import org.leumasjaffe.charsheet.model.magic.DDSpellbook;
+import org.leumasjaffe.format.StringFormatter;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 
@@ -29,6 +34,11 @@ import lombok.experimental.FieldDefaults;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @JsonIgnoreProperties(ignoreUnknown=true)
 public class DDClass {
+	static ObjectMapper mapper = new ObjectMapper();
+	static {
+		mapper.registerModule(new Jdk8Module());
+	}
+
 	@NonNull String name;
 	
 	int skillPoints;
@@ -41,6 +51,7 @@ public class DDClass {
 	
 	@NonNull Set<String> skills;
 	
+	String spellBookType;
 	@NonNull Optional<DDSpellList> spells;
 	
 	static Map<String, DDClass> store = new HashMap<>();
@@ -48,9 +59,8 @@ public class DDClass {
 	@SneakyThrows
 	public static DDClass getFromResource(final String name) {
 		if (!store.containsKey(name)) {
-			final ObjectMapper mapper = new ObjectMapper();
-			mapper.registerModule(new Jdk8Module());
-			store.put(name, mapper.readValue(new File("resources/classes/" + name + ".json"), DDClass.class));
+			store.put(name, mapper.readValue(new File("resources/classes/" + name + ".json"), 
+					DDClass.class));
 		}
 		return store.get(name);
 	}
@@ -70,4 +80,16 @@ public class DDClass {
 	public List<DDProperty> getFeatures(int level) {
 		return features.size() > level ? Collections.unmodifiableList(features.get(level)) : Collections.emptyList();
 	}
+
+	public Optional<DDSpellbook> createNewSpellBook()  {
+		return spells.flatMap(sl -> sl.getSpellBookTypeName()).map(st -> createSpellbookImpl(st));
+	}
+
+	@SneakyThrows({JsonMappingException.class, JsonParseException.class, IOException.class})
+	private DDSpellbook createSpellbookImpl(String st) {
+		if (!st.startsWith("{")) {
+			st = new StringFormatter("{{\"@c\":\"{}\"}}").format(st);
+		}
+		return mapper.readValue(st.getBytes(), DDSpellbook.class);
+	}
 }

+ 6 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/DDSpellList.java

@@ -1,10 +1,13 @@
 package org.leumasjaffe.charsheet.model.magic;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
 
 import lombok.AccessLevel;
 import lombok.Data;
@@ -15,10 +18,12 @@ import lombok.experimental.FieldDefaults;
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 @JsonIgnoreProperties(ignoreUnknown=true)
 public class DDSpellList {
+	Optional<String> spellBookTypeName;
 	@NonNull Source group;
 	@NonNull String ability;
 	
-	@NonNull List<List<Integer>> known;
+	@JsonInclude(JsonInclude.Include.NON_NULL)
+	@NonNull List<List<Integer>> known = Collections.emptyList();
 	@NonNull List<List<Integer>> perDay;
 
 	@NonNull List<SpellList> spellList;

+ 2 - 1
src/main/lombok/org/leumasjaffe/charsheet/model/magic/impl/Inspired.java

@@ -20,6 +20,7 @@ public class Inspired extends Prepared {
 	@AllArgsConstructor
 	@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 	private static class Level {
+		Level() { this(Collections.emptyList(), Collections.emptyList(), 0); }
 		@NonNull List<DDSpell> spellsPrepared, spellsPreparedPreviously;
 		@NonFinal int spellsPerDay;
 	}
@@ -64,7 +65,7 @@ public class Inspired extends Prepared {
 	}
 	
 	private Level get(int level) {
-		return spellInfo.getOrDefault(level, new Level(Collections.emptyList(), Collections.emptyList(), 0));
+		return spellInfo.getOrDefault(level, new Level());
 	}
 
 	@Override

+ 1 - 3
src/main/lombok/org/leumasjaffe/charsheet/view/DeveloperMenu.java

@@ -1,7 +1,5 @@
 package org.leumasjaffe.charsheet.view;
 
-import java.util.stream.Collectors;
-
 import javax.swing.JFrame;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
@@ -27,7 +25,7 @@ public class DeveloperMenu extends JMenu {
 		JMenuItem mntmLUSkill = new JMenuItem("Level Up - Skill");
 		mntmLUSkill.addActionListener(e -> {
 			Object[] choices = model[0].getClasses().stream().map(DDCharacterClass::getName)
-					.collect(Collectors.toList()).toArray();
+					.toArray(String[]::new);
 			String clazz = (String) JOptionPane.showInputDialog(this.getParent(), 
 					"Which Class is Leveling Up?", "Level Up - Skill", 
 					JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]);

+ 77 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/ChooseClassLevelUpDialog.java

@@ -0,0 +1,77 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+
+import java.awt.GridBagLayout;
+
+import javax.swing.JComboBox;
+import java.awt.GridBagConstraints;
+import javax.swing.JLabel;
+import java.awt.Insets;
+import java.util.function.Consumer;
+
+import javax.swing.JButton;
+
+@SuppressWarnings("serial")
+class ChooseClassLevelUpDialog extends JPanel {
+	public ChooseClassLevelUpDialog(DDCharacter chara, Consumer<LevelUpClassInfo> nexter) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0, 0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{0.0, 0.0, 1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{0.0, 0.0, 1.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		
+		JLabel lblClass = new JLabel("Class:");
+		GridBagConstraints gbc_lblClass = new GridBagConstraints();
+		gbc_lblClass.insets = new Insets(0, 0, 5, 5);
+		gbc_lblClass.anchor = GridBagConstraints.EAST;
+		gbc_lblClass.gridx = 1;
+		gbc_lblClass.gridy = 1;
+		add(lblClass, gbc_lblClass);
+
+		String[] choices = chara.getClasses().stream()
+				.map(DDCharacterClass::getName).toArray(String[]::new);
+		JComboBox<String> comboBox = new JComboBox<>(choices);
+		comboBox.setEditable(true);
+		GridBagConstraints gbc_comboBox = new GridBagConstraints();
+		gbc_comboBox.insets = new Insets(0, 0, 5, 0);
+		gbc_comboBox.fill = GridBagConstraints.HORIZONTAL;
+		gbc_comboBox.gridx = 2;
+		gbc_comboBox.gridy = 1;
+		add(comboBox, gbc_comboBox);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.gridwidth = 2;
+		gbc_panel.insets = new Insets(0, 0, 0, 5);
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 1;
+		gbc_panel.gridy = 2;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JButton btnNext = new JButton("Next");
+		GridBagConstraints gbc_btnNext = new GridBagConstraints();
+		gbc_btnNext.gridx = 1;
+		gbc_btnNext.gridy = 0;
+		panel.add(btnNext, gbc_btnNext);
+		btnNext.addActionListener(e -> {
+			final String name = (String) comboBox.getSelectedItem();
+			final LevelUpClassInfo info = chara.getClasses().stream()
+					.filter(c -> c.getName().equals(name)).findFirst()
+					.map(dch -> new LevelUpClassInfo(chara, dch.clone(), dch.getLevel().value() + 1))
+					.orElseGet(() -> new LevelUpClassInfo(chara, new DDCharacterClass(name)));
+			nexter.accept(info);
+		});
+	}
+}

+ 19 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpClassInfo.java

@@ -0,0 +1,19 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import org.leumasjaffe.charsheet.model.DDCharacter;
+import org.leumasjaffe.charsheet.model.DDCharacterClass;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@AllArgsConstructor
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PUBLIC)
+class LevelUpClassInfo {
+	@NonNull DDCharacter ddCharacter;
+	@NonNull DDCharacterClass ddClass;
+	int toLevel = 1;
+}

+ 80 - 1
src/main/lombok/org/leumasjaffe/charsheet/view/level/LevelUpDialog.java

@@ -6,9 +6,20 @@ import javax.swing.JPanel;
 
 import org.leumasjaffe.charsheet.model.DDCharacter;
 
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+
 @SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
 public class LevelUpDialog extends JPanel {
-	private static final int[] EXPERIENCE_CACHE;
+	static int[] EXPERIENCE_CACHE;
 	static {
 		final int levelsToCalc = 20;
 		EXPERIENCE_CACHE = new int[levelsToCalc];
@@ -17,6 +28,11 @@ public class LevelUpDialog extends JPanel {
 		}
 	}
 	
+	ChooseClassLevelUpDialog chooseClass;
+	JButton btnCommit;
+	@NonFinal JPanel updateClass;
+	private GridBagConstraints gbc_main;
+	
 	public static int computeLevelsNeeded(DDCharacter chara, int bonusLevels) {
 		final int exp = chara.getExperience().value();
 		final int currentLevel = chara.getLevel();
@@ -25,6 +41,69 @@ public class LevelUpDialog extends JPanel {
 	}
 	
 	public LevelUpDialog(DDCharacter chara) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		chooseClass = new ChooseClassLevelUpDialog(chara, this::createLayer2);
+		gbc_main = new GridBagConstraints();
+		gbc_main.insets = new Insets(0, 0, 5, 0);
+		gbc_main.fill = GridBagConstraints.BOTH;
+		gbc_main.gridx = 0;
+		gbc_main.gridy = 0;
+		add(chooseClass, gbc_main);
+				
+		JPanel panel_1 = new JPanel();
+		GridBagConstraints gbc_panel_1 = new GridBagConstraints();
+		gbc_panel_1.fill = GridBagConstraints.BOTH;
+		gbc_panel_1.gridx = 0;
+		gbc_panel_1.gridy = 1;
+		add(panel_1, gbc_panel_1);
+		GridBagLayout gbl_panel_1 = new GridBagLayout();
+		gbl_panel_1.columnWidths = new int[]{0, 0, 0, 0, 0};
+		gbl_panel_1.rowHeights = new int[]{0, 0};
+		gbl_panel_1.columnWeights = new double[]{1.0, 0.0, 0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel_1.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel_1.setLayout(gbl_panel_1);
+		
+		JButton btnCancel = new JButton("Cancel");
+		GridBagConstraints gbc_btnCancel = new GridBagConstraints();
+		gbc_btnCancel.insets = new Insets(0, 0, 0, 5);
+		gbc_btnCancel.gridx = 1;
+		gbc_btnCancel.gridy = 0;
+		panel_1.add(btnCancel, gbc_btnCancel);
+		btnCancel.addActionListener(e -> ((JDialog) this.getParent().getParent().getParent()).dispose());
+		
+		btnCommit = new JButton("Commit");
+		GridBagConstraints gbc_btnCommit = new GridBagConstraints();
+		gbc_btnCommit.insets = new Insets(0, 0, 0, 5);
+		gbc_btnCommit.gridx = 2;
+		gbc_btnCommit.gridy = 0;
+		panel_1.add(btnCommit, gbc_btnCommit);
+		btnCommit.setEnabled(false);
+		btnCommit.addActionListener(e -> {
+			// TODO
+		});
+	}
+	
+	void dropLayer2() {
+		remove(updateClass);
+		add(chooseClass, gbc_main);
+		repaint();
+		revalidate();
+		((JDialog) this.getParent().getParent().getParent()).pack();
+	}
+	
+	void createLayer2(LevelUpClassInfo info) {
+		remove(chooseClass);
+		updateClass = new UpdateClassWithLevelPanel(info, this::dropLayer2, b -> btnCommit.setEnabled(b));
+		add(updateClass, gbc_main);
+		revalidate();
+		repaint();
+		((JDialog) this.getParent().getParent().getParent()).pack();
 	}
 	
 	private static int experienceForLevel(int level) {

+ 93 - 0
src/main/lombok/org/leumasjaffe/charsheet/view/level/UpdateClassWithLevelPanel.java

@@ -0,0 +1,93 @@
+package org.leumasjaffe.charsheet.view.level;
+
+import java.util.function.Consumer;
+import java.util.stream.IntStream;
+
+import javax.swing.JPanel;
+
+import org.leumasjaffe.charsheet.view.skills.SkillLevelUpPanel;
+import org.leumasjaffe.function.VoidVoidFunction;
+import org.leumasjaffe.observer.Observable;
+import org.leumasjaffe.observer.ObservableListener;
+import org.leumasjaffe.observer.ObserverDispatch;
+
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+import java.awt.GridBagLayout;
+import javax.swing.JTabbedPane;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.Component;
+import javax.swing.Box;
+import javax.swing.JButton;
+
+@SuppressWarnings("serial")
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+class UpdateClassWithLevelPanel extends JPanel {
+	@FieldDefaults(level=AccessLevel.PUBLIC, makeFinal=true)
+	public class BoolArray extends Observable {
+		boolean data[] = new boolean[] {false};
+	}
+	BoolArray readyCount = new BoolArray();
+	ObservableListener<Consumer<Boolean>, BoolArray> listener;
+	public UpdateClassWithLevelPanel(LevelUpClassInfo info, VoidVoidFunction back,
+			Consumer<Boolean> setReady) {
+		GridBagLayout gridBagLayout = new GridBagLayout();
+		gridBagLayout.columnWidths = new int[]{0, 0};
+		gridBagLayout.rowHeights = new int[]{0, 0, 0};
+		gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
+		gridBagLayout.rowWeights = new double[]{1.0, 0.0, Double.MIN_VALUE};
+		setLayout(gridBagLayout);
+		
+		JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP);
+		GridBagConstraints gbc_tabbedPane = new GridBagConstraints();
+		gbc_tabbedPane.insets = new Insets(0, 0, 5, 0);
+		gbc_tabbedPane.fill = GridBagConstraints.BOTH;
+		gbc_tabbedPane.gridx = 0;
+		gbc_tabbedPane.gridy = 0;
+		add(tabbedPane, gbc_tabbedPane);
+		
+		JPanel skills = new SkillLevelUpPanel(info.ddCharacter, info.ddClass) {
+			@Override public void setIsReady(boolean b) {
+				readyCount.data[0] = b;
+				ObserverDispatch.notifySubscribers(readyCount, null);
+			}
+		};
+		tabbedPane.addTab("Skills", null, skills, null);
+		
+		JPanel panel = new JPanel();
+		GridBagConstraints gbc_panel = new GridBagConstraints();
+		gbc_panel.fill = GridBagConstraints.BOTH;
+		gbc_panel.gridx = 0;
+		gbc_panel.gridy = 1;
+		add(panel, gbc_panel);
+		GridBagLayout gbl_panel = new GridBagLayout();
+		gbl_panel.columnWidths = new int[]{0, 0, 0};
+		gbl_panel.rowHeights = new int[]{0, 0};
+		gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
+		gbl_panel.rowWeights = new double[]{0.0, Double.MIN_VALUE};
+		panel.setLayout(gbl_panel);
+		
+		JButton btnBack = new JButton("Back");
+		GridBagConstraints gbc_btnBack = new GridBagConstraints();
+		gbc_btnBack.insets = new Insets(0, 0, 0, 5);
+		gbc_btnBack.gridx = 0;
+		gbc_btnBack.gridy = 0;
+		panel.add(btnBack, gbc_btnBack);
+		btnBack.addActionListener(e -> back.apply());
+		
+		Component horizontalGlue = Box.createHorizontalGlue();
+		GridBagConstraints gbc_horizontalGlue = new GridBagConstraints();
+		gbc_horizontalGlue.gridx = 1;
+		gbc_horizontalGlue.gridy = 0;
+		panel.add(horizontalGlue, gbc_horizontalGlue);
+		
+		listener = new ObservableListener<>(setReady, (c, v) -> {
+			c.accept(IntStream.range(0, v.data.length)
+					.mapToObj(i -> v.data[i]).allMatch(b -> b));
+		});
+		listener.setObserved(readyCount);
+	}
+}