Better Games In Match Selection (#8098)

* Add GamesInMatch combo box selection to booster draft page

Also updated combo box default to be seeded with the stored preferences

* Working comboboxes for desktop version

* Working linked buttons on Mobile

* Add binder classes for preferences and other Model components

* Move to pref binders for mobile GUI
This commit is contained in:
Matthew Scott Krafczyk
2025-07-25 09:07:19 -05:00
committed by GitHub
parent 79845eff1d
commit ab2b06500b
11 changed files with 220 additions and 17 deletions

View File

@@ -0,0 +1,26 @@
package forge.gui;
import javax.swing.*;
import java.awt.event.ItemEvent;
import forge.localinstance.properties.ForgePreferences;
import forge.model.FPrefsBinder;
public class SwingPrefBinders {
public static final class ComboBox extends FPrefsBinder<JComboBox<String>, String> {
public ComboBox(ForgePreferences.FPref key, JComboBox<String> box) {
super(
key,
box,
b -> (String) b.getSelectedItem(),
(b, s) -> b.setSelectedItem(s),
s -> s,
s -> s);
box.addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
this.save();
}
});
}
}
}

View File

@@ -54,6 +54,7 @@ public class CLobby {
// General updates when switching back to this view
view.getBtnStart().requestFocusInWindow();
});
view.getGamesInMatchBinder().load();
}
public void initialize() {

View File

@@ -34,6 +34,7 @@ import forge.gamemodes.match.LobbySlotType;
import forge.gamemodes.net.event.UpdateLobbyPlayerEvent;
import forge.gui.CardDetailPanel;
import forge.gui.GuiBase;
import forge.gui.SwingPrefBinders;
import forge.gui.interfaces.ILobbyView;
import forge.gui.util.SOptionPane;
import forge.interfaces.IPlayerChangeListener;
@@ -74,7 +75,9 @@ public class VLobby implements ILobbyView {
private final StartButton btnStart = new StartButton();
private final JPanel pnlStart = new JPanel(new MigLayout("insets 0, gap 0, wrap 2"));
private final JComboBox gamesInMatch = new JComboBox(new String[] {"1","3","5"});
private final JComboBox<String> gamesInMatch = new JComboBox<String>(new String[] {"1","3","5"});
private final SwingPrefBinders.ComboBox gamesInMatchBinder =
new SwingPrefBinders.ComboBox(FPref.UI_MATCHES_PER_GAME, gamesInMatch);
private final JPanel gamesInMatchFrame = new JPanel(new MigLayout("insets 0, gap 0, wrap 2"));
private final JPanel constructedFrame = new JPanel(new MigLayout("insets 0, gap 0, wrap 2")); // Main content frame
@@ -182,17 +185,19 @@ public class VLobby implements ILobbyView {
btnStart.addActionListener(arg0 -> {
Runnable startGame = lobby.startGame();
if (startGame != null) {
if (!gamesInMatch.getSelectedItem().equals(FPref.UI_MATCHES_PER_GAME)) {
FModel.getPreferences().setPref(FPref.UI_MATCHES_PER_GAME, (String) gamesInMatch.getSelectedItem());
}
startGame.run();
}
});
}
String defaultGamesInMatch = FModel.getPreferences().getPref(FPref.UI_MATCHES_PER_GAME);
if (defaultGamesInMatch == null || defaultGamesInMatch.isEmpty()) {
defaultGamesInMatch = "3";
}
gamesInMatchFrame.add(newLabel(localizer.getMessage("lblGamesInMatch")), "w 150px!, h 30px!");
gamesInMatchFrame.add(gamesInMatch, "w 50px!, h 30px!");
gamesInMatchFrame.setOpaque(false);
gamesInMatch.setSelectedItem("3");
pnlStart.add(gamesInMatchFrame);
}
@@ -855,6 +860,11 @@ public class VLobby implements ILobbyView {
return nonRandomAiAvatars;
}
/** Return the gamesInMatchBinder */
public SwingPrefBinders.ComboBox getGamesInMatchBinder() {
return gamesInMatchBinder;
}
/** Populate vanguard lists. */
private void populateVanguardLists() {
humanListData.add("Use deck's default avatar (random if unavailable)");

View File

@@ -97,12 +97,15 @@ public enum CSubmenuDraft implements ICDoc {
view.getBtnBuildDeck().requestFocusInWindow();
}
});
view.getGamesInMatchBinder().load();
}
private void startGame(final GameType gameType) {
final Localizer localizer = Localizer.getInstance();
final boolean gauntlet = VSubmenuDraft.SINGLETON_INSTANCE.isGauntlet();
final DeckProxy humanDeck = VSubmenuDraft.SINGLETON_INSTANCE.getLstDecks().getSelectedItem();
final VSubmenuDraft view = VSubmenuDraft.SINGLETON_INSTANCE;
final boolean gauntlet = view.isGauntlet();
final DeckProxy humanDeck = view.getLstDecks().getSelectedItem();
if (humanDeck == null) {
FOptionPane.showErrorDialog(localizer.getMessage("lblNoDeckSelected"), localizer.getMessage("lblNoDeck"));
@@ -245,5 +248,4 @@ public enum CSubmenuDraft implements ICDoc {
combo.addItem("5");
}
}
}

View File

@@ -13,6 +13,7 @@ import forge.game.GameType;
import forge.gui.framework.DragCell;
import forge.gui.framework.DragTab;
import forge.gui.framework.EDocID;
import forge.gui.SwingPrefBinders;
import forge.itemmanager.DeckManager;
import forge.itemmanager.ItemManagerContainer;
import forge.screens.deckeditor.CDeckEditorUI;
@@ -26,6 +27,8 @@ import forge.toolbox.FLabel;
import forge.toolbox.FRadioButton;
import forge.toolbox.FSkin;
import forge.toolbox.JXButtonPanel;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.model.FModel;
import forge.util.Localizer;
import net.miginfocom.swing.MigLayout;
@@ -57,6 +60,11 @@ public enum VSubmenuDraft implements IVSubmenu<CSubmenuDraft> {
private final JComboBox<String> cbOpponent = new JComboBox<>();
private final JComboBox<String> gamesInMatch = new JComboBox<String>(new String[] {"1","3","5"});
private final SwingPrefBinders.ComboBox gamesInMatchBinder =
new SwingPrefBinders.ComboBox(FPref.UI_MATCHES_PER_GAME, gamesInMatch);
private final JPanel gamesInMatchFrame = new JPanel(new MigLayout("insets 0, gap 0, wrap 2"));
private final JLabel lblInfo = new FLabel.Builder()
.fontAlign(SwingConstants.LEFT).fontSize(16).fontStyle(Font.BOLD)
.text(localizer.getMessage("lblBuildorselectadeck")).build();
@@ -73,6 +81,10 @@ public enum VSubmenuDraft implements IVSubmenu<CSubmenuDraft> {
.text(localizer.getMessage("lblDraftText3"))
.fontSize(12).build();
private final FLabel lblGamesInMatch = new FLabel.Builder()
.text(localizer.getMessage("lblGamesInMatch"))
.fontSize(12).build();
private final FLabel btnBuildDeck = new FLabel.ButtonBuilder().text(localizer.getMessage("lblNewBoosterDraftGame")).fontSize(16).build();
/**
@@ -91,10 +103,22 @@ public enum VSubmenuDraft implements IVSubmenu<CSubmenuDraft> {
radSingle.setSelected(true);
grpPanel.add(cbOpponent, "w 200px!, h 30px!");
pnlStart.setLayout(new MigLayout("insets 0, gap 0, wrap 2"));
pnlStart.setLayout(new MigLayout("insets 0, gap 0",
"[grow][pref!]",
"[pref!][grow,fill][pref!]"));
pnlStart.setOpaque(false);
pnlStart.add(grpPanel, "gapright 20");
pnlStart.add(btnStart);
pnlStart.add(grpPanel, "cell 0 0 1 3, growy, gapright 20");
String defaultGamesInMatch = FModel.getPreferences().getPref(FPref.UI_MATCHES_PER_GAME);
if (defaultGamesInMatch == null || defaultGamesInMatch.isEmpty()) {
defaultGamesInMatch = "3";
}
gamesInMatchFrame.add(lblGamesInMatch, "w 150px!, h 30px!");
gamesInMatchFrame.add(gamesInMatch, "w 50px!, h 30px!");
gamesInMatchFrame.setOpaque(false);
pnlStart.add(gamesInMatchFrame, "cell 1 0, alignx center, aligny top");
pnlStart.add(btnStart, "cell 1 2, alignx center, aligny bottom");
}
/* (non-Javadoc)
@@ -147,6 +171,7 @@ public enum VSubmenuDraft implements IVSubmenu<CSubmenuDraft> {
public JRadioButton getRadSingle() { return radSingle; }
public JRadioButton getRadMultiple() { return radMultiple; }
public JRadioButton getRadAll() { return radAll; }
public SwingPrefBinders.ComboBox getGamesInMatchBinder() { return gamesInMatchBinder; }
//========== Overridden from IVDoc

View File

@@ -51,6 +51,7 @@ import forge.toolbox.FScrollPane;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.Utils;
import forge.util.GuiPrefBinders;
public abstract class LobbyScreen extends LaunchScreen implements ILobbyView {
private static final ForgePreferences prefs = FModel.getPreferences();
@@ -72,6 +73,8 @@ public abstract class LobbyScreen extends LaunchScreen implements ILobbyView {
// Max games in a match frame and variables
private final FLabel lblGamesInMatch = new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblMatch") + ":").font(VARIANTS_FONT).build();
private final FComboBox<String> cbGamesInMatch = new FComboBox<>();
private final GuiPrefBinders.ComboBox cbGamesInMatchBinder =
new GuiPrefBinders.ComboBox(FPref.UI_MATCHES_PER_GAME, cbGamesInMatch);
private final List<PlayerPanel> playerPanels = new ArrayList<>(MAX_PLAYERS);
private final FScrollPane playersScroll = new FScrollPane() {
@@ -133,8 +136,6 @@ public abstract class LobbyScreen extends LaunchScreen implements ILobbyView {
cbGamesInMatch.addItem("1");
cbGamesInMatch.addItem("3");
cbGamesInMatch.addItem("5");
cbGamesInMatch.setSelectedItem(FModel.getPreferences().getPref((FPref.UI_MATCHES_PER_GAME)));
cbGamesInMatch.setChangedHandler(event -> FModel.getPreferences().setPref(FPref.UI_MATCHES_PER_GAME, cbGamesInMatch.getSelectedItem()));
add(lblVariants);
add(cbVariants);
@@ -588,6 +589,11 @@ public abstract class LobbyScreen extends LaunchScreen implements ILobbyView {
}
}
@Override
public void onActivate() {
cbGamesInMatchBinder.load();
}
@Override
public void update(final boolean fullUpdate) {
int playerCount = lobby.getNumberOfSlots();

View File

@@ -20,6 +20,7 @@ import forge.gamemodes.match.HostedMatch;
import forge.gui.FThreads;
import forge.gui.GuiBase;
import forge.gui.util.SGuiChoose;
import forge.util.GuiPrefBinders;
import forge.itemmanager.DeckManager;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.filters.ItemFilter;
@@ -32,6 +33,7 @@ import forge.screens.home.LoadGameMenu;
import forge.toolbox.FComboBox;
import forge.toolbox.FLabel;
import forge.toolbox.FOptionPane;
import forge.util.Utils;
public class LoadDraftScreen extends LaunchScreen {
private final DeckManager lstDecks = add(new DeckManager(GameType.Draft));
@@ -44,6 +46,12 @@ public class LoadDraftScreen extends LaunchScreen {
private final FLabel lblMode = add(new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblMode")).font(GAME_MODE_FONT).build());
private final FComboBox<String> cbMode = add(new FComboBox<>());
// Max games in a match frame and variables
private final FLabel lblGamesInMatch = add(new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblMatch") + ":").font(GAME_MODE_FONT).build());
private final FComboBox<String> cbGamesInMatch = add(new FComboBox<>());
private final GuiPrefBinders.ComboBox cbGamesInMatchBinder = new GuiPrefBinders.ComboBox(
FPref.UI_MATCHES_PER_GAME, cbGamesInMatch);
public LoadDraftScreen() {
super(null, LoadGameMenu.getMenu());
@@ -53,12 +61,18 @@ public class LoadDraftScreen extends LaunchScreen {
lstDecks.setup(ItemManagerConfig.DRAFT_DECKS);
lstDecks.setItemActivateHandler(event -> editSelectedDeck());
cbGamesInMatch.setFont(GAME_MODE_FONT);
cbGamesInMatch.addItem("1");
cbGamesInMatch.addItem("3");
cbGamesInMatch.addItem("5");
}
@Override
public void onActivate() {
lstDecks.setPool(DeckProxy.getAllDraftDecks());
lstDecks.setSelectedString(DeckPreferences.getDraftDeck());
cbGamesInMatchBinder.load();
}
private void editSelectedDeck() {
@@ -78,8 +92,16 @@ public class LoadDraftScreen extends LaunchScreen {
float listHeight = height - labelHeight - y - FDeckChooser.PADDING;
float comboBoxHeight = cbMode.getHeight();
lblMode.setBounds(x, y, lblMode.getAutoSizeBounds().width + FDeckChooser.PADDING / 2, comboBoxHeight);
cbMode.setBounds(x + lblMode.getWidth(), y, w - lblMode.getWidth(), comboBoxHeight);
float x2 = x;
float w1 = lblMode.getAutoSizeBounds().width;
float w2 = lblGamesInMatch.getAutoSizeBounds().width;
lblMode.setBounds(x2, y, w1 + FDeckChooser.PADDING / 2, comboBoxHeight);
x2 += lblMode.getWidth();
cbMode.setBounds(x2, y, w - x2 - w2 - Utils.AVG_FINGER_WIDTH, comboBoxHeight);
x2 += cbMode.getWidth();
lblGamesInMatch.setBounds(x2, y, w2 + FDeckChooser.PADDING / 2, comboBoxHeight);
x2 += lblGamesInMatch.getWidth();
cbGamesInMatch.setBounds(x2, y, Utils.AVG_FINGER_WIDTH, comboBoxHeight);
y += comboBoxHeight + FDeckChooser.PADDING;
lstDecks.setBounds(x, y, w, listHeight);
y += listHeight + FDeckChooser.PADDING;

View File

@@ -20,6 +20,7 @@ import forge.gamemodes.match.HostedMatch;
import forge.gui.FThreads;
import forge.gui.GuiBase;
import forge.gui.util.SGuiChoose;
import forge.util.GuiPrefBinders;
import forge.itemmanager.DeckManager;
import forge.itemmanager.ItemManagerConfig;
import forge.itemmanager.filters.ItemFilter;
@@ -32,6 +33,7 @@ import forge.screens.home.LoadGameMenu;
import forge.toolbox.FComboBox;
import forge.toolbox.FLabel;
import forge.toolbox.FOptionPane;
import forge.util.Utils;
public class LoadSealedScreen extends LaunchScreen {
private final DeckManager lstDecks = add(new DeckManager(GameType.Draft));
@@ -44,6 +46,12 @@ public class LoadSealedScreen extends LaunchScreen {
private final FLabel lblMode = add(new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblMode")).font(GAME_MODE_FONT).build());
private final FComboBox<String> cbMode = add(new FComboBox<>());
// Max games in a match frame and variables
private final FLabel lblGamesInMatch = add(new FLabel.Builder().text(Forge.getLocalizer().getMessage("lblMatch") + ":").font(GAME_MODE_FONT).build());
private final FComboBox<String> cbGamesInMatch = add(new FComboBox<>());
private final GuiPrefBinders.ComboBox cbGamesInMatchBinder = new GuiPrefBinders.ComboBox(
FPref.UI_MATCHES_PER_GAME, cbGamesInMatch);
public LoadSealedScreen() {
super(null, LoadGameMenu.getMenu());
@@ -53,12 +61,18 @@ public class LoadSealedScreen extends LaunchScreen {
lstDecks.setup(ItemManagerConfig.SEALED_DECKS);
lstDecks.setItemActivateHandler(event -> editSelectedDeck());
cbGamesInMatch.setFont(GAME_MODE_FONT);
cbGamesInMatch.addItem("1");
cbGamesInMatch.addItem("3");
cbGamesInMatch.addItem("5");
}
@Override
public void onActivate() {
lstDecks.setPool(DeckProxy.getAllSealedDecks());
lstDecks.setSelectedString(DeckPreferences.getSealedDeck());
cbGamesInMatchBinder.load();
}
private void editSelectedDeck() {
@@ -78,8 +92,16 @@ public class LoadSealedScreen extends LaunchScreen {
float listHeight = height - labelHeight - y - FDeckChooser.PADDING;
float comboBoxHeight = cbMode.getHeight();
lblMode.setBounds(x, y, lblMode.getAutoSizeBounds().width + FDeckChooser.PADDING / 2, comboBoxHeight);
cbMode.setBounds(x + lblMode.getWidth(), y, w - lblMode.getWidth(), comboBoxHeight);
float x2 = x;
float w1 = lblMode.getAutoSizeBounds().width;
float w2 = lblGamesInMatch.getAutoSizeBounds().width;
lblMode.setBounds(x2, y, w1 + FDeckChooser.PADDING / 2, comboBoxHeight);
x2 += lblMode.getWidth();
cbMode.setBounds(x2, y, w - x2 - w2 - Utils.AVG_FINGER_WIDTH, comboBoxHeight);
x2 += cbMode.getWidth();
lblGamesInMatch.setBounds(x2, y, w2 + FDeckChooser.PADDING / 2, comboBoxHeight);
x2 += lblGamesInMatch.getWidth();
cbGamesInMatch.setBounds(x2, y, Utils.AVG_FINGER_WIDTH, comboBoxHeight);
y += comboBoxHeight + FDeckChooser.PADDING;
lstDecks.setBounds(x, y, w, listHeight);
y += listHeight + FDeckChooser.PADDING;

View File

@@ -0,0 +1,23 @@
package forge.util;
import forge.toolbox.FComboBox;
import forge.localinstance.properties.ForgePreferences;
import forge.model.FPrefsBinder;
public class GuiPrefBinders {
public static final class ComboBox extends FPrefsBinder<FComboBox<String>, String> {
public ComboBox(ForgePreferences.FPref key, FComboBox<String> box) {
super(
key,
box,
b -> (String) b.getSelectedItem(),
(b, s) -> b.setSelectedItem(s),
s -> s,
s -> s);
box.setChangedHandler(e -> {
this.save();
});
}
}
}

View File

@@ -0,0 +1,56 @@
package forge.model;
import java.util.function.BiConsumer;
import java.util.function.Function;
import forge.localinstance.properties.ForgePreferences;
/**
* Binds any GUI component to a Forge preference key
*/
public class FPrefsBinder<C, V> implements ModelBinder<ForgePreferences.FPref, C, V> {
private final ForgePreferences.FPref prefKey;
private final C component;
/** Extracts a value from the component */
private final Function<C, V> extractor;
/** Pushes a value back into the component */
private final BiConsumer<C, V> applier;
/** conversions between pref store value and component value */
private final Function<V, String> toString;
private final Function<String, V> fromString;
public FPrefsBinder(ForgePreferences.FPref prefKey,
C component,
Function<C, V> extractor,
BiConsumer<C, V> applier,
Function<V, String> toString,
Function<String, V> fromString) {
this.prefKey = prefKey;
this.component = component;
this.extractor = extractor;
this.applier = applier;
this.toString = toString;
this.fromString = fromString;
}
public void load() {
ForgePreferences prefs = FModel.getPreferences();
String prefValue = prefs.getPref(prefKey);
if (prefValue != null) {
V value = fromString.apply(prefValue);
applier.accept(component, value);
}
}
public void save() {
ForgePreferences prefs = FModel.getPreferences();
V value = extractor.apply(component);
String prefValue = toString.apply(value);
prefs.setPref(prefKey, prefValue);
prefs.save();
}
}

View File

@@ -0,0 +1,10 @@
package forge.model;
/**
* Binds any GUI component to a Forge preference key
*/
public interface ModelBinder<K, C, V> {
public void load();
public void save();
}