From a33b1ee516b910e68dea983aab6b2c078506e693 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sat, 20 Dec 2025 16:15:39 +0100 Subject: [PATCH 1/6] GUI: Change deck selection to combobox Also remember a history of the last 5 used decks to make deck selection faster. --- .../client/preference/MagePreferences.java | 31 ++++++- .../mage/client/table/NewPlayerPanel.java | 81 +++++++++++-------- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java b/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java index e3a3c5ed4f3..d0dd77e52d8 100644 --- a/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java +++ b/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java @@ -4,10 +4,11 @@ import com.google.common.collect.Sets; import mage.client.MageFrame; import mage.client.util.ClientDefaultSettings; -import java.util.Date; -import java.util.Set; +import java.io.File; +import java.util.*; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import java.util.stream.Collectors; // TODO: Move all preference related logic from MageFrame and PreferencesDialog to this class. public final class MagePreferences { @@ -181,4 +182,30 @@ public final class MagePreferences { public static String getLastServerPassword() { return lastServerPassword.isEmpty() ? getPassword(getLastServerAddress()) : lastServerPassword; } + + public static List getRecentDeckFiles() { + int limit = prefs().getInt("recentDeckFilesLimit", 5); + if (limit <= 0) + return new ArrayList<>(); + + return Arrays.stream(prefs().get("recentDeckFiles", "").split(";")).filter(filename -> { + File file = new File(filename); + return file.canRead(); + }).limit(limit).collect(Collectors.toList()); + } + + public static void putRecentDeckFile(String filename) { + if (filename.isEmpty()) + return; + + File file = new File(filename); + if (!file.canRead()) + return; + + List current = new ArrayList<>(getRecentDeckFiles()); + current.removeIf(filename::equals); + current.add(0, filename); + + prefs().put("recentDeckFiles", String.join(";", current)); + } } diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index 6db3835601f..fe93ca610b6 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -5,8 +5,8 @@ import java.io.IOException; import javax.swing.*; import mage.cards.decks.DeckFileFilter; -import mage.client.MageFrame; import mage.client.deck.generator.DeckGenerator; +import mage.client.preference.MagePreferences; import mage.client.util.ClientDefaultSettings; /** @@ -22,31 +22,48 @@ public class NewPlayerPanel extends javax.swing.JPanel { fcSelectDeck = new JFileChooser(); fcSelectDeck.setAcceptAllFileFilterUsed(false); fcSelectDeck.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); - this.txtPlayerDeck.setText(""); - this.txtPlayerName.setText(ClientDefaultSettings.computerName); + cbPlayerDeck.setEditable(true);; + txtPlayerName.setText(ClientDefaultSettings.computerName); + + loadRecentDeckFiles(); + + // If the user selection changes, move the last selected item to the + // front of the list. + cbPlayerDeck.addActionListener(event -> { + try { + String selection = cbPlayerDeck.getSelectedItem().toString(); + MagePreferences.putRecentDeckFile(selection); + } catch (NullPointerException ex) { /* Ignore */ } + }); + } + + public void loadRecentDeckFiles() { + cbPlayerDeck.removeAllItems(); + for (String filename : MagePreferences.getRecentDeckFiles()) { + cbPlayerDeck.addItem(filename); + } + cbPlayerDeck.setSelectedIndex(0); } public void setPlayerName(String playerName) { - this.txtPlayerName.setText(playerName); - this.txtPlayerName.setEditable(false); - this.txtPlayerName.setEnabled(false); + txtPlayerName.setText(playerName); + txtPlayerName.setEditable(false); + txtPlayerName.setEnabled(false); } - protected void playerLoadDeck() { - String lastFolder = MageFrame.getPreferences().get("lastDeckFolder", ""); - if (!lastFolder.isEmpty()) { - fcSelectDeck.setCurrentDirectory(new File(lastFolder)); - } + protected void playerLoadDeck() + { + File currentFile = new File(cbPlayerDeck.getEditor().getItem().toString()); + fcSelectDeck.setCurrentDirectory(currentFile); + int ret = fcSelectDeck.showDialog(this, "Select Deck"); if (ret == JFileChooser.APPROVE_OPTION) { File file = fcSelectDeck.getSelectedFile(); - this.txtPlayerDeck.setText(file.getPath()); try { - MageFrame.getPreferences().put("lastDeckFolder", file.getCanonicalPath()); - } catch (IOException ex) { - } + MagePreferences.putRecentDeckFile(file.getCanonicalPath()); + loadRecentDeckFiles(); + } catch (IOException ex) { /* Ignore */ } } - fcSelectDeck.setSelectedFile(null); } protected void generateDeck() { @@ -54,23 +71,23 @@ public class NewPlayerPanel extends javax.swing.JPanel { if (path == null) { return; } - this.txtPlayerDeck.setText(path); + cbPlayerDeck.getEditor().setItem(path); } public String getPlayerName() { - return this.txtPlayerName.getText(); + return txtPlayerName.getText(); } public String getDeckFile() { - return this.txtPlayerDeck.getText(); + return cbPlayerDeck.getEditor().getItem().toString(); } - public void setDeckFile(String deckFile) { - this.txtPlayerDeck.setText(deckFile); + public void setDeckFile(String filename) { + cbPlayerDeck.getEditor().setItem(filename); } public void setSkillLevel(int level) { - this.spnLevel.setValue(level); + spnLevel.setValue(level); } public int getSkillLevel() { @@ -78,15 +95,15 @@ public class NewPlayerPanel extends javax.swing.JPanel { } public void showLevel(boolean show) { - this.spnLevel.setVisible(show); - this.lblLevel.setVisible(show); + spnLevel.setVisible(show); + lblLevel.setVisible(show); } public void showDeckElements(boolean show) { - this.lblPlayerDeck.setVisible(show); - this.txtPlayerDeck.setVisible(show); - this.btnGenerate.setVisible(show); - this.btnPlayerDeck.setVisible(show); + lblPlayerDeck.setVisible(show); + cbPlayerDeck.setVisible(show); + btnGenerate.setVisible(show); + btnPlayerDeck.setVisible(show); } /** @@ -101,7 +118,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { lblPlayerName = new javax.swing.JLabel(); txtPlayerName = new javax.swing.JTextField(); lblPlayerDeck = new javax.swing.JLabel(); - txtPlayerDeck = new javax.swing.JTextField(); + cbPlayerDeck = new javax.swing.JComboBox(); btnPlayerDeck = new javax.swing.JButton(); btnGenerate = new javax.swing.JButton(); lblLevel = new javax.swing.JLabel(); @@ -134,7 +151,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(txtPlayerName, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE) - .addComponent(txtPlayerDeck, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE)) + .addComponent(cbPlayerDeck, javax.swing.GroupLayout.DEFAULT_SIZE, 310, javax.swing.GroupLayout.DEFAULT_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addGroup(layout.createSequentialGroup() @@ -158,7 +175,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(lblPlayerDeck) - .addComponent(txtPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(cbPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(btnPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE))) ); @@ -179,7 +196,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { private javax.swing.JLabel lblPlayerDeck; private javax.swing.JLabel lblPlayerName; private javax.swing.JSpinner spnLevel; - private javax.swing.JTextField txtPlayerDeck; + private javax.swing.JComboBox cbPlayerDeck; private javax.swing.JTextField txtPlayerName; // End of variables declaration//GEN-END:variables From 665b5cb1fe06c6a1bd6d545c3829b6071763519f Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sat, 20 Dec 2025 21:07:51 +0100 Subject: [PATCH 2/6] GUI: Improve recent decks list - Support resizing the JoinTable dialog - Fix re-loading the list of recent decks - Add tooltips that show the full path - Add a context menu to clear the list of recent decks --- .../mage/client/dialog/JoinTableDialog.java | 6 +- .../mage/client/dialog/NewTableDialog.java | 3 +- .../client/dialog/NewTournamentDialog.java | 5 +- .../mage/client/dialog/PreferencesDialog.java | 4 + .../client/preference/MagePreferences.java | 26 --- .../mage/client/table/NewPlayerPanel.java | 174 +++++++++++++++--- .../mage/client/table/TablePlayerPanel.java | 1 + 7 files changed, 166 insertions(+), 53 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/dialog/JoinTableDialog.java b/Mage.Client/src/main/java/mage/client/dialog/JoinTableDialog.java index f5725dc588a..81c8c522391 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/JoinTableDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/JoinTableDialog.java @@ -8,6 +8,7 @@ import mage.remote.Session; import org.apache.log4j.Logger; import javax.swing.*; +import java.awt.*; import java.util.UUID; /** @@ -39,12 +40,15 @@ public class JoinTableDialog extends MageDialog { this.isTournament = isTournament; this.newPlayerPanel.setPlayerName(SessionHandler.getUserName()); this.newPlayerPanel.showDeckElements(!isLimited); + this.newPlayerPanel.loadRecentDeckFiles(); this.setModal(true); this.setLocation(100, 100); this.setVisible(true); + this.setResizable(true); + this.setMinimumSize(this.getPreferredSize()); + this.setMaximumSize(new Dimension(Short.MAX_VALUE, this.getPreferredSize().height)); } - /** * This method is called from within the constructor to * initialize the form. diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java index 532ea79bc9d..823ba8aed79 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTableDialog.java @@ -835,7 +835,8 @@ public class NewTableDialog extends MageDialog { this.roomId = roomId; if (!lastSessionId.equals(SessionHandler.getSessionId())) { lastSessionId = SessionHandler.getSessionId(); - this.player1Panel.setPlayerName(SessionHandler.getUserName()); + player1Panel.setPlayerName(SessionHandler.getUserName()); + player1Panel.loadRecentDeckFiles(); cbGameType.setModel(new DefaultComboBoxModel(SessionHandler.getGameTypes().toArray())); cbDeckType.setModel(new DefaultComboBoxModel(SessionHandler.getDeckTypes())); selectLimitedByDefault(); diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java index b83408d981a..c7d900c3b8a 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java @@ -97,8 +97,9 @@ public class NewTournamentDialog extends MageDialog { this.roomId = roomId; if (!lastSessionId.equals(SessionHandler.getSessionId())) { lastSessionId = SessionHandler.getSessionId(); - this.player1Panel.setPlayerName(SessionHandler.getUserName()); - this.player1Panel.showLevel(false); // no computer + player1Panel.setPlayerName(SessionHandler.getUserName()); + player1Panel.showLevel(false); // no computer + player1Panel.loadRecentDeckFiles(); cbTournamentType.setModel(new DefaultComboBoxModel(SessionHandler.getTournamentTypes().toArray())); cbGameType.setModel(new DefaultComboBoxModel(SessionHandler.getTournamentGameTypes().toArray())); diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 13004ab13bf..a03300ece8b 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -330,6 +330,10 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_CONNECT_AUTO_CONNECT = "autoConnect"; public static final String KEY_CONNECT_FLAG = "connectFlag"; + // Settings for the New/Join Table Dialog + public static final String KEY_RECENT_DECKLIST_FILES = "recentDecklistFiles"; // Semicolon separated list of filenames + public static final String KEY_MAX_RECENT_DECKLIST_FLIES = "numRecentDecklistFiles"; // Maximum number of recent decks to remember + // auto-update settings on first run public static final String KEY_SETTINGS_VERSION = "settingsVersion"; diff --git a/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java b/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java index d0dd77e52d8..4c8779a8a5d 100644 --- a/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java +++ b/Mage.Client/src/main/java/mage/client/preference/MagePreferences.java @@ -182,30 +182,4 @@ public final class MagePreferences { public static String getLastServerPassword() { return lastServerPassword.isEmpty() ? getPassword(getLastServerAddress()) : lastServerPassword; } - - public static List getRecentDeckFiles() { - int limit = prefs().getInt("recentDeckFilesLimit", 5); - if (limit <= 0) - return new ArrayList<>(); - - return Arrays.stream(prefs().get("recentDeckFiles", "").split(";")).filter(filename -> { - File file = new File(filename); - return file.canRead(); - }).limit(limit).collect(Collectors.toList()); - } - - public static void putRecentDeckFile(String filename) { - if (filename.isEmpty()) - return; - - File file = new File(filename); - if (!file.canRead()) - return; - - List current = new ArrayList<>(getRecentDeckFiles()); - current.removeIf(filename::equals); - current.add(0, filename); - - prefs().put("recentDeckFiles", String.join(";", current)); - } } diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index fe93ca610b6..494d569474e 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -1,12 +1,22 @@ package mage.client.table; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.Preferences; +import java.util.stream.Collectors; import javax.swing.*; import mage.cards.decks.DeckFileFilter; +import mage.client.MageFrame; import mage.client.deck.generator.DeckGenerator; -import mage.client.preference.MagePreferences; +import mage.client.dialog.PreferencesDialog; import mage.client.util.ClientDefaultSettings; /** @@ -22,27 +32,131 @@ public class NewPlayerPanel extends javax.swing.JPanel { fcSelectDeck = new JFileChooser(); fcSelectDeck.setAcceptAllFileFilterUsed(false); fcSelectDeck.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); - cbPlayerDeck.setEditable(true);; txtPlayerName.setText(ClientDefaultSettings.computerName); + cbPlayerDeck.setEditable(true); - loadRecentDeckFiles(); + // We set some arbitrary width to prevent the combobox from growing + // very wide for long paths. + cbPlayerDeck.setPrototypeDisplayValue("xxxxxxxxxxxxxx"); + + // Set up tooltips for combobox items so very long paths don't get truncated. + cbPlayerDeck.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value != null) { + if (c instanceof JComponent) { + ((JComponent) c).setToolTipText(value.toString()); + } + } + + return c; + } + }); + + // Add a context menu to the combobox that allows clearing the list + // of recent decks. + JPopupMenu deckContextMenu = new JPopupMenu(); + JMenuItem clearItem = new JMenuItem("Clear recent decks"); + clearItem.addActionListener(event -> { + clearRecentDeckFiles(); + }); + deckContextMenu.add(clearItem); + + cbPlayerDeck.getEditor().getEditorComponent().addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { showContext(e); } + @Override + public void mouseReleased(MouseEvent e) { showContext(e); } + + public void showContext(MouseEvent event) { + if (event.isPopupTrigger()) { + deckContextMenu.show(event.getComponent(), event.getX(), event.getY()); + } + } + }); // If the user selection changes, move the last selected item to the // front of the list. - cbPlayerDeck.addActionListener(event -> { - try { - String selection = cbPlayerDeck.getSelectedItem().toString(); - MagePreferences.putRecentDeckFile(selection); - } catch (NullPointerException ex) { /* Ignore */ } + cbPlayerDeck.addItemListener(event -> { + if (event.getStateChange() == ItemEvent.SELECTED) { + Object selection = event.getItem(); + if (selection != null) { + putRecentDeckFile(selection.toString()); + } + } }); + + loadRecentDeckFiles(); } + /** + * Clears the list of recent deck files and removes all items from the combobox + */ + private void clearRecentDeckFiles() { + Preferences prefs = MageFrame.getPreferences(); + prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, ""); + cbPlayerDeck.removeAllItems(); + } + + /** + * Get the list of recently used deck filenames. + * Only filenames that point to readable files are returned. + * + * @return A list of filenames + */ + private List getRecentDeckFiles() { + Preferences prefs = MageFrame.getPreferences(); + + int limit = prefs.getInt(PreferencesDialog.KEY_MAX_RECENT_DECKLIST_FLIES, 10); + if (limit <= 0) { + return new ArrayList<>(); + } else { + return Arrays.stream(prefs.get(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, "").split(";")) + .limit(limit) + .filter(filename -> { + File file = new File(filename); + return file.canRead(); + }) + .collect(Collectors.toList()); + } + } + + /** + * Put a filename to the list of recent decks. + * + * @param filename Filename of the deck to add + */ + private void putRecentDeckFile(String filename) { + if (filename == null || filename.isEmpty() || filename.contains(";")) + return; + + File file = new File(filename); + if (!file.canRead()) + return; + + List current = new ArrayList<>(getRecentDeckFiles()); + current.removeIf(filename::equals); + current.add(0, filename); + + Preferences prefs = MageFrame.getPreferences(); + prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, String.join(";", current)); + } + + /** + * Fill the combobox with the recent n filenames. + */ public void loadRecentDeckFiles() { cbPlayerDeck.removeAllItems(); - for (String filename : MagePreferences.getRecentDeckFiles()) { + for (String filename : getRecentDeckFiles()) cbPlayerDeck.addItem(filename); - } - cbPlayerDeck.setSelectedIndex(0); + + if (cbPlayerDeck.getItemCount() > 0) + cbPlayerDeck.setSelectedIndex(0); } public void setPlayerName(String playerName) { @@ -50,19 +164,26 @@ public class NewPlayerPanel extends javax.swing.JPanel { txtPlayerName.setEditable(false); txtPlayerName.setEnabled(false); } - - protected void playerLoadDeck() - { - File currentFile = new File(cbPlayerDeck.getEditor().getItem().toString()); - fcSelectDeck.setCurrentDirectory(currentFile); + + /** + * Called when the user presses the [...] button to select a deck file. + */ + protected void playerLoadDeck() { + Object item = cbPlayerDeck.getEditor().getItem(); + if (item != null) { + File currentFile = new File(item.toString()); + fcSelectDeck.setCurrentDirectory(currentFile); + } int ret = fcSelectDeck.showDialog(this, "Select Deck"); if (ret == JFileChooser.APPROVE_OPTION) { File file = fcSelectDeck.getSelectedFile(); + try { - MagePreferences.putRecentDeckFile(file.getCanonicalPath()); + putRecentDeckFile(file.getCanonicalPath()); loadRecentDeckFiles(); - } catch (IOException ex) { /* Ignore */ } + } catch (IOException ignore) { + } } } @@ -71,7 +192,9 @@ public class NewPlayerPanel extends javax.swing.JPanel { if (path == null) { return; } - cbPlayerDeck.getEditor().setItem(path); + + putRecentDeckFile(path); + loadRecentDeckFiles(); } public String getPlayerName() { @@ -79,11 +202,16 @@ public class NewPlayerPanel extends javax.swing.JPanel { } public String getDeckFile() { - return cbPlayerDeck.getEditor().getItem().toString(); + Object item = cbPlayerDeck.getEditor().getItem(); + if (item != null) { + return item.toString(); + } + return ""; } public void setDeckFile(String filename) { - cbPlayerDeck.getEditor().setItem(filename); + putRecentDeckFile(filename); + loadRecentDeckFiles(); } public void setSkillLevel(int level) { @@ -118,7 +246,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { lblPlayerName = new javax.swing.JLabel(); txtPlayerName = new javax.swing.JTextField(); lblPlayerDeck = new javax.swing.JLabel(); - cbPlayerDeck = new javax.swing.JComboBox(); + cbPlayerDeck = new javax.swing.JComboBox<>(); btnPlayerDeck = new javax.swing.JButton(); btnGenerate = new javax.swing.JButton(); lblLevel = new javax.swing.JLabel(); @@ -151,7 +279,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(txtPlayerName, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE) - .addComponent(cbPlayerDeck, javax.swing.GroupLayout.DEFAULT_SIZE, 310, javax.swing.GroupLayout.DEFAULT_SIZE)) + .addComponent(cbPlayerDeck, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addGroup(layout.createSequentialGroup() diff --git a/Mage.Client/src/main/java/mage/client/table/TablePlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/TablePlayerPanel.java index 61c7614e6ad..f4e4dd0c0b7 100644 --- a/Mage.Client/src/main/java/mage/client/table/TablePlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/TablePlayerPanel.java @@ -129,6 +129,7 @@ public class TablePlayerPanel extends javax.swing.JPanel { if (getPlayerType() != PlayerType.HUMAN) { this.newPlayerPanel.setVisible(true); this.newPlayerPanel.setPlayerName(extractAiPlayerNumberFromLabel(this.lblPlayerNum.getText())); + this.newPlayerPanel.loadRecentDeckFiles(); } else { this.newPlayerPanel.setVisible(false); } From bc1d9be26dccf0ee87cdfcdab8facba5c839cfd0 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Sun, 21 Dec 2025 01:34:54 +0100 Subject: [PATCH 3/6] GUI: Move recent-decklist logic to a new file --- .../client/dialog/NewTournamentDialog.java | 5 +- .../mage/client/table/NewPlayerPanel.java | 176 +------------- .../mage/client/util/RecentDecklistUtil.java | 217 ++++++++++++++++++ 3 files changed, 229 insertions(+), 169 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java diff --git a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java index c7d900c3b8a..10098ef288d 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/NewTournamentDialog.java @@ -18,6 +18,7 @@ import mage.client.MageFrame; import mage.client.SessionHandler; import mage.client.table.TournamentPlayerPanel; import mage.client.util.IgnoreList; +import mage.client.util.RecentDecklistUtil; import mage.client.util.gui.FastSearchUtil; import mage.constants.*; import mage.game.GameException; @@ -810,7 +811,7 @@ public class NewTournamentDialog extends MageDialog { fcSelectDeck.setAcceptAllFileFilterUsed(false); fcSelectDeck.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); } - String lastFolder = MageFrame.getPreferences().get("lastDeckFolder", ""); + String lastFolder = RecentDecklistUtil.getRecentDecklistDir(); if (!lastFolder.isEmpty()) { fcSelectDeck.setCurrentDirectory(new File(lastFolder)); } @@ -830,7 +831,7 @@ public class NewTournamentDialog extends MageDialog { fcJumpstartSelectDeck.setAcceptAllFileFilterUsed(false); fcJumpstartSelectDeck.addChoosableFileFilter(new DeckFileFilter("txt", "Jumpstart Packs (*.txt)")); } - String lastFolder = MageFrame.getPreferences().get("lastDeckFolder", ""); + String lastFolder = RecentDecklistUtil.getRecentDecklistDir(); if (!lastFolder.isEmpty()) { fcJumpstartSelectDeck.setCurrentDirectory(new File(lastFolder)); } diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index 494d569474e..e2f22fd2471 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -1,162 +1,30 @@ package mage.client.table; import java.awt.*; -import java.awt.event.ItemEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.prefs.Preferences; -import java.util.stream.Collectors; import javax.swing.*; -import mage.cards.decks.DeckFileFilter; -import mage.client.MageFrame; import mage.client.deck.generator.DeckGenerator; -import mage.client.dialog.PreferencesDialog; import mage.client.util.ClientDefaultSettings; +import mage.client.util.RecentDecklistUtil; /** * * @author BetaSteward_at_googlemail.com */ public class NewPlayerPanel extends javax.swing.JPanel { - - private final JFileChooser fcSelectDeck; + private final RecentDecklistUtil recentDecklistUtil; public NewPlayerPanel() { initComponents(); - fcSelectDeck = new JFileChooser(); - fcSelectDeck.setAcceptAllFileFilterUsed(false); - fcSelectDeck.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); txtPlayerName.setText(ClientDefaultSettings.computerName); - cbPlayerDeck.setEditable(true); - - // We set some arbitrary width to prevent the combobox from growing - // very wide for long paths. - cbPlayerDeck.setPrototypeDisplayValue("xxxxxxxxxxxxxx"); - - // Set up tooltips for combobox items so very long paths don't get truncated. - cbPlayerDeck.setRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, - Object value, - int index, - boolean isSelected, - boolean cellHasFocus) { - Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value != null) { - if (c instanceof JComponent) { - ((JComponent) c).setToolTipText(value.toString()); - } - } - - return c; - } - }); - - // Add a context menu to the combobox that allows clearing the list - // of recent decks. - JPopupMenu deckContextMenu = new JPopupMenu(); - JMenuItem clearItem = new JMenuItem("Clear recent decks"); - clearItem.addActionListener(event -> { - clearRecentDeckFiles(); - }); - deckContextMenu.add(clearItem); - - cbPlayerDeck.getEditor().getEditorComponent().addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { showContext(e); } - @Override - public void mouseReleased(MouseEvent e) { showContext(e); } - - public void showContext(MouseEvent event) { - if (event.isPopupTrigger()) { - deckContextMenu.show(event.getComponent(), event.getX(), event.getY()); - } - } - }); - - // If the user selection changes, move the last selected item to the - // front of the list. - cbPlayerDeck.addItemListener(event -> { - if (event.getStateChange() == ItemEvent.SELECTED) { - Object selection = event.getItem(); - if (selection != null) { - putRecentDeckFile(selection.toString()); - } - } - }); - - loadRecentDeckFiles(); - } - - /** - * Clears the list of recent deck files and removes all items from the combobox - */ - private void clearRecentDeckFiles() { - Preferences prefs = MageFrame.getPreferences(); - prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, ""); - cbPlayerDeck.removeAllItems(); - } - - /** - * Get the list of recently used deck filenames. - * Only filenames that point to readable files are returned. - * - * @return A list of filenames - */ - private List getRecentDeckFiles() { - Preferences prefs = MageFrame.getPreferences(); - - int limit = prefs.getInt(PreferencesDialog.KEY_MAX_RECENT_DECKLIST_FLIES, 10); - if (limit <= 0) { - return new ArrayList<>(); - } else { - return Arrays.stream(prefs.get(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, "").split(";")) - .limit(limit) - .filter(filename -> { - File file = new File(filename); - return file.canRead(); - }) - .collect(Collectors.toList()); - } - } - - /** - * Put a filename to the list of recent decks. - * - * @param filename Filename of the deck to add - */ - private void putRecentDeckFile(String filename) { - if (filename == null || filename.isEmpty() || filename.contains(";")) - return; - - File file = new File(filename); - if (!file.canRead()) - return; - - List current = new ArrayList<>(getRecentDeckFiles()); - current.removeIf(filename::equals); - current.add(0, filename); - - Preferences prefs = MageFrame.getPreferences(); - prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, String.join(";", current)); + recentDecklistUtil = new RecentDecklistUtil(cbPlayerDeck); } /** * Fill the combobox with the recent n filenames. */ public void loadRecentDeckFiles() { - cbPlayerDeck.removeAllItems(); - for (String filename : getRecentDeckFiles()) - cbPlayerDeck.addItem(filename); - - if (cbPlayerDeck.getItemCount() > 0) - cbPlayerDeck.setSelectedIndex(0); + recentDecklistUtil.update(); } public void setPlayerName(String playerName) { @@ -169,32 +37,11 @@ public class NewPlayerPanel extends javax.swing.JPanel { * Called when the user presses the [...] button to select a deck file. */ protected void playerLoadDeck() { - Object item = cbPlayerDeck.getEditor().getItem(); - if (item != null) { - File currentFile = new File(item.toString()); - fcSelectDeck.setCurrentDirectory(currentFile); - } - - int ret = fcSelectDeck.showDialog(this, "Select Deck"); - if (ret == JFileChooser.APPROVE_OPTION) { - File file = fcSelectDeck.getSelectedFile(); - - try { - putRecentDeckFile(file.getCanonicalPath()); - loadRecentDeckFiles(); - } catch (IOException ignore) { - } - } + recentDecklistUtil.chooseFile(); } protected void generateDeck() { - String path = DeckGenerator.generateDeck(); - if (path == null) { - return; - } - - putRecentDeckFile(path); - loadRecentDeckFiles(); + recentDecklistUtil.setFile(DeckGenerator.generateDeck()); } public String getPlayerName() { @@ -202,16 +49,11 @@ public class NewPlayerPanel extends javax.swing.JPanel { } public String getDeckFile() { - Object item = cbPlayerDeck.getEditor().getItem(); - if (item != null) { - return item.toString(); - } - return ""; + return recentDecklistUtil.getFile(); } public void setDeckFile(String filename) { - putRecentDeckFile(filename); - loadRecentDeckFiles(); + recentDecklistUtil.setFile(filename); } public void setSkillLevel(int level) { @@ -219,7 +61,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { } public int getSkillLevel() { - return (Integer) spnLevel.getValue(); + return (Integer)spnLevel.getValue(); } public void showLevel(boolean show) { diff --git a/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java b/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java new file mode 100644 index 00000000000..f529d8c4eef --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java @@ -0,0 +1,217 @@ +package mage.client.util; + +import mage.cards.decks.DeckFileFilter; +import mage.client.MageFrame; +import mage.client.dialog.PreferencesDialog; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.Preferences; +import java.util.stream.Collectors; + +public class RecentDecklistUtil { + static private final String CLEAR_ITEM = "Clear list"; + + private boolean inUpdate = false; + final private JComboBox control; + + /** + * Construct a new utility to manage a JComboBox for choosing a decklist, + * either by selecting a file or by choosing a recent selection. + * + * @param control The combobox to use + */ + public RecentDecklistUtil(JComboBox control) { + this.control = control; + initControl(); + } + + /** + * Setup the combobox control + */ + private void initControl() { + control.setEditable(true); + + // We set some arbitrary width to prevent the combobox from growing + // very wide for long paths. + control.setPrototypeDisplayValue("xxxxxxxxxxxxxx"); + + control.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent event) { + if (!inUpdate) { + if (event.getStateChange() == ItemEvent.SELECTED) { + Object selection = event.getItem(); + if (selection == CLEAR_ITEM) { + clear(); + } else if (selection != null) { + putRecentDecklistFiles(selection.toString()); + update(); + } + } + } + } + }); + + // Set up tooltips for combobox items so very long paths don't get truncated. + control.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + JLabel c = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value != null) { + c.setToolTipText(value.toString()); + } + + return c; + } + }); + } + + /** + * Clear all recent files. + */ + public void clear() { + clearRecentDecklistFiles(); + update(); + } + + /** + * Update the control. + */ + public void update() { + try { + inUpdate = true; + control.removeAllItems(); + + List files = getRecentDecklistFiles(); + for (String filename : files) + control.addItem(filename); + + if (!files.isEmpty()) + control.addItem(CLEAR_ITEM); + + if (control.getItemCount() > 0) + control.setSelectedIndex(0); + } finally { + inUpdate = false; + } + } + + /** + * Sets the current file to the filename given. + * Also adds that file to the recent list. + * + * @param filename The filename to add + */ + public void setFile(String filename) { + putRecentDecklistFiles(filename); + update(); + } + + /** + * @return The currently entered/selected filename + */ + public String getFile() { + Object selection = control.getEditor().getItem(); + if (selection != null) + return selection.toString(); + return ""; + } + + /** + * Let the user select a file on their system by presenting + * a file chooser. + */ + public void chooseFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setAcceptAllFileFilterUsed(false); + chooser.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); + + Object item = control.getEditor().getItem(); + if (item != null) { + File currentFile = new File(item.toString()); + chooser.setCurrentDirectory(currentFile); + } + + int ret = chooser.showDialog(control, "Select Deck"); + if (ret == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + try { + putRecentDecklistFiles(file.getCanonicalPath()); + update(); + } catch (IOException ignore) { + } + } + } + + /** + * @return The most recent decklist directory + */ + public static String getRecentDecklistDir() { + List list = getRecentDecklistFiles(); + if (!list.isEmpty()) { + File file = new File(list.get(0)); + return file.getParent(); + } + return ""; + } + + /** + * Get the list of recently used deck filenames. + * Only filenames that point to readable files are returned. + * + * @return A list of filenames + */ + public static List getRecentDecklistFiles() { + final int limit = 10; + + Preferences prefs = MageFrame.getPreferences(); + return Arrays.stream(prefs.get(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, "").split(";")) + .limit(limit) + .filter(filename -> { + File file = new File(filename); + return file.canRead() && file.isFile(); + }) + .collect(Collectors.toList()); + } + + /** + * Clears the list of recent deck files and removes all items from the combobox + */ + public static void clearRecentDecklistFiles() { + Preferences prefs = MageFrame.getPreferences(); + prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, ""); + } + + /** + * Put a filename to the list of recent decks. + * + * @param filename Filename of the deck to add + */ + public static void putRecentDecklistFiles(String filename) { + if (filename == null || filename.isEmpty() || filename.contains(";")) + return; + + File file = new File(filename); + if (!file.canRead()) + return; + + List current = new ArrayList<>(getRecentDecklistFiles()); + current.removeIf(filename::equals); + current.add(0, filename); + + Preferences prefs = MageFrame.getPreferences(); + prefs.put(PreferencesDialog.KEY_RECENT_DECKLIST_FILES, String.join(";", current)); + } +} From 0862be5d1c8355475c1df065c22f5fbbbb661630 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 22 Dec 2025 13:22:02 +0100 Subject: [PATCH 4/6] GUI: Move decklist selection to DecklistChooser --- .../client/components/DecklistChooser.java | 166 ++++++++++++++++++ .../mage/client/table/NewPlayerPanel.java | 43 ++--- .../mage/client/util/RecentDecklistUtil.java | 144 --------------- 3 files changed, 177 insertions(+), 176 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/components/DecklistChooser.java diff --git a/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java new file mode 100644 index 00000000000..b09d20940d8 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java @@ -0,0 +1,166 @@ +package mage.client.components; + +import mage.cards.decks.DeckFileFilter; +import mage.client.util.RecentDecklistUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class DecklistChooser extends JPanel +{ + private final String CLEAR_ITEM = "Clear recent decks"; + + private final JButton chooseButton; + private final JComboBox decklistCombobox; + private boolean inUpdate = false; + + public DecklistChooser() { + chooseButton = new JButton("..."); + chooseButton.setToolTipText("Select deck file..."); + + decklistCombobox = new JComboBox<>(); + + setupControls(); + update(); + } + + private void setupControls() { + chooseButton.addActionListener(e -> chooseFile()); + + decklistCombobox.setEditable(true); + + // We set some arbitrary width to prevent the combobox from growing + // very wide for long paths. + decklistCombobox.setPrototypeDisplayValue("xxxxxxxxxxxxxx"); + + decklistCombobox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent event) { + if (!inUpdate) { + if (event.getStateChange() == ItemEvent.SELECTED) { + Object selection = event.getItem(); + if (selection == CLEAR_ITEM) { + clear(); + } else if (selection != null) { + RecentDecklistUtil.putRecentDecklistFiles(selection.toString()); + update(); + } + } + } + } + }); + + // Set up tooltips for combobox items so very long paths don't get truncated. + decklistCombobox.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + JLabel c = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value != null) { + c.setToolTipText(value.toString()); + } + + return c; + } + }); + + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(decklistCombobox, gbc); + + gbc.gridx = 1; + gbc.weightx = 0; + gbc.fill = GridBagConstraints.NONE; + add(chooseButton, gbc); + } + + /** + * Update the control. + */ + public void update() { + try { + inUpdate = true; + decklistCombobox.removeAllItems(); + + List files = RecentDecklistUtil.getRecentDecklistFiles(); + for (String filename : files) + decklistCombobox.addItem(filename); + + if (!files.isEmpty()) + decklistCombobox.addItem(CLEAR_ITEM); + + if (decklistCombobox.getItemCount() > 0) + decklistCombobox.setSelectedIndex(0); + } finally { + inUpdate = false; + } + } + + /** + * Clear all recent files. + */ + public void clear() { + RecentDecklistUtil.clearRecentDecklistFiles(); + update(); + } + + /** + * Sets the current file to the filename given. + * Also adds that file to the recent list. + * + * @param filename The filename to add + */ + public void setFile(String filename) { + RecentDecklistUtil.putRecentDecklistFiles(filename); + update(); + } + + /** + * @return The currently entered/selected filename + */ + public String getFile() { + Object selection = decklistCombobox.getEditor().getItem(); + if (selection != null) + return selection.toString(); + return ""; + } + + /** + * Let the user select a file on their system by presenting + * a file chooser. + */ + public void chooseFile() { + JFileChooser chooser = new JFileChooser(); + chooser.setAcceptAllFileFilterUsed(false); + chooser.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); + + Object item = decklistCombobox.getEditor().getItem(); + if (item != null) { + File currentFile = new File(item.toString()); + chooser.setCurrentDirectory(currentFile); + } + + int ret = chooser.showDialog(decklistCombobox, "Select Deck"); + if (ret == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + try { + RecentDecklistUtil.putRecentDecklistFiles(file.getCanonicalPath()); + update(); + } catch (IOException ignore) { + } + } + } +} diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index e2f22fd2471..61061795a7c 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -3,28 +3,26 @@ package mage.client.table; import java.awt.*; import javax.swing.*; +import mage.client.components.DecklistChooser; import mage.client.deck.generator.DeckGenerator; import mage.client.util.ClientDefaultSettings; -import mage.client.util.RecentDecklistUtil; /** * * @author BetaSteward_at_googlemail.com */ public class NewPlayerPanel extends javax.swing.JPanel { - private final RecentDecklistUtil recentDecklistUtil; public NewPlayerPanel() { initComponents(); txtPlayerName.setText(ClientDefaultSettings.computerName); - recentDecklistUtil = new RecentDecklistUtil(cbPlayerDeck); } /** * Fill the combobox with the recent n filenames. */ public void loadRecentDeckFiles() { - recentDecklistUtil.update(); + decklist.update(); } public void setPlayerName(String playerName) { @@ -33,15 +31,8 @@ public class NewPlayerPanel extends javax.swing.JPanel { txtPlayerName.setEnabled(false); } - /** - * Called when the user presses the [...] button to select a deck file. - */ - protected void playerLoadDeck() { - recentDecklistUtil.chooseFile(); - } - protected void generateDeck() { - recentDecklistUtil.setFile(DeckGenerator.generateDeck()); + decklist.setFile(DeckGenerator.generateDeck()); } public String getPlayerName() { @@ -49,11 +40,11 @@ public class NewPlayerPanel extends javax.swing.JPanel { } public String getDeckFile() { - return recentDecklistUtil.getFile(); + return decklist.getFile(); } public void setDeckFile(String filename) { - recentDecklistUtil.setFile(filename); + decklist.setFile(filename); } public void setSkillLevel(int level) { @@ -71,9 +62,8 @@ public class NewPlayerPanel extends javax.swing.JPanel { public void showDeckElements(boolean show) { lblPlayerDeck.setVisible(show); - cbPlayerDeck.setVisible(show); + decklist.setVisible(show); btnGenerate.setVisible(show); - btnPlayerDeck.setVisible(show); } /** @@ -88,8 +78,7 @@ public class NewPlayerPanel extends javax.swing.JPanel { lblPlayerName = new javax.swing.JLabel(); txtPlayerName = new javax.swing.JTextField(); lblPlayerDeck = new javax.swing.JLabel(); - cbPlayerDeck = new javax.swing.JComboBox<>(); - btnPlayerDeck = new javax.swing.JButton(); + decklist = new DecklistChooser(); btnGenerate = new javax.swing.JButton(); lblLevel = new javax.swing.JLabel(); spnLevel = new javax.swing.JSpinner(); @@ -98,9 +87,6 @@ public class NewPlayerPanel extends javax.swing.JPanel { lblPlayerDeck.setText("Deck:"); - btnPlayerDeck.setText("..."); - btnPlayerDeck.addActionListener(evt -> btnPlayerDeckActionPerformed(evt)); - btnGenerate.setText("Generate"); btnGenerate.addActionListener(evt -> btnGenerateActionPerformed(evt)); @@ -121,11 +107,10 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(txtPlayerName, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE) - .addComponent(cbPlayerDeck, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE)) + .addComponent(decklist, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addGroup(layout.createSequentialGroup() - .addComponent(btnPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, 87, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(layout.createSequentialGroup() @@ -145,28 +130,22 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(lblPlayerDeck) - .addComponent(cbPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(btnPlayerDeck, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(decklist, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) ); }// //GEN-END:initComponents - private void btnPlayerDeckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnPlayerDeckActionPerformed - playerLoadDeck(); -}//GEN-LAST:event_btnPlayerDeckActionPerformed - private void btnGenerateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGenerateActionPerformed generateDeck(); }//GEN-LAST:event_btnGenerateActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton btnGenerate; - private javax.swing.JButton btnPlayerDeck; + private DecklistChooser decklist; private javax.swing.JLabel lblLevel; private javax.swing.JLabel lblPlayerDeck; private javax.swing.JLabel lblPlayerName; private javax.swing.JSpinner spnLevel; - private javax.swing.JComboBox cbPlayerDeck; private javax.swing.JTextField txtPlayerName; // End of variables declaration//GEN-END:variables diff --git a/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java b/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java index f529d8c4eef..7b50f5909f5 100644 --- a/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java +++ b/Mage.Client/src/main/java/mage/client/util/RecentDecklistUtil.java @@ -1,15 +1,9 @@ package mage.client.util; -import mage.cards.decks.DeckFileFilter; import mage.client.MageFrame; import mage.client.dialog.PreferencesDialog; -import javax.swing.*; -import java.awt.*; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -17,144 +11,6 @@ import java.util.prefs.Preferences; import java.util.stream.Collectors; public class RecentDecklistUtil { - static private final String CLEAR_ITEM = "Clear list"; - - private boolean inUpdate = false; - final private JComboBox control; - - /** - * Construct a new utility to manage a JComboBox for choosing a decklist, - * either by selecting a file or by choosing a recent selection. - * - * @param control The combobox to use - */ - public RecentDecklistUtil(JComboBox control) { - this.control = control; - initControl(); - } - - /** - * Setup the combobox control - */ - private void initControl() { - control.setEditable(true); - - // We set some arbitrary width to prevent the combobox from growing - // very wide for long paths. - control.setPrototypeDisplayValue("xxxxxxxxxxxxxx"); - - control.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent event) { - if (!inUpdate) { - if (event.getStateChange() == ItemEvent.SELECTED) { - Object selection = event.getItem(); - if (selection == CLEAR_ITEM) { - clear(); - } else if (selection != null) { - putRecentDecklistFiles(selection.toString()); - update(); - } - } - } - } - }); - - // Set up tooltips for combobox items so very long paths don't get truncated. - control.setRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, - Object value, - int index, - boolean isSelected, - boolean cellHasFocus) { - JLabel c = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value != null) { - c.setToolTipText(value.toString()); - } - - return c; - } - }); - } - - /** - * Clear all recent files. - */ - public void clear() { - clearRecentDecklistFiles(); - update(); - } - - /** - * Update the control. - */ - public void update() { - try { - inUpdate = true; - control.removeAllItems(); - - List files = getRecentDecklistFiles(); - for (String filename : files) - control.addItem(filename); - - if (!files.isEmpty()) - control.addItem(CLEAR_ITEM); - - if (control.getItemCount() > 0) - control.setSelectedIndex(0); - } finally { - inUpdate = false; - } - } - - /** - * Sets the current file to the filename given. - * Also adds that file to the recent list. - * - * @param filename The filename to add - */ - public void setFile(String filename) { - putRecentDecklistFiles(filename); - update(); - } - - /** - * @return The currently entered/selected filename - */ - public String getFile() { - Object selection = control.getEditor().getItem(); - if (selection != null) - return selection.toString(); - return ""; - } - - /** - * Let the user select a file on their system by presenting - * a file chooser. - */ - public void chooseFile() { - JFileChooser chooser = new JFileChooser(); - chooser.setAcceptAllFileFilterUsed(false); - chooser.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)")); - - Object item = control.getEditor().getItem(); - if (item != null) { - File currentFile = new File(item.toString()); - chooser.setCurrentDirectory(currentFile); - } - - int ret = chooser.showDialog(control, "Select Deck"); - if (ret == JFileChooser.APPROVE_OPTION) { - File file = chooser.getSelectedFile(); - try { - putRecentDecklistFiles(file.getCanonicalPath()); - update(); - } catch (IOException ignore) { - } - } - } - /** * @return The most recent decklist directory */ From a6f1c194ebc1590422c4af6050488a150fff099c Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 23 Dec 2025 11:51:34 +0100 Subject: [PATCH 5/6] GUI: Add "Generate" to the new Decklist Panel --- .../client/components/DecklistChooser.java | 20 +++++++++++++++++++ .../mage/client/table/NewPlayerPanel.java | 16 +-------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java index b09d20940d8..fe7515c889b 100644 --- a/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java +++ b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java @@ -1,6 +1,7 @@ package mage.client.components; import mage.cards.decks.DeckFileFilter; +import mage.client.deck.generator.DeckGenerator; import mage.client.util.RecentDecklistUtil; import javax.swing.*; @@ -16,6 +17,7 @@ public class DecklistChooser extends JPanel private final String CLEAR_ITEM = "Clear recent decks"; private final JButton chooseButton; + private final JButton generateButton; private final JComboBox decklistCombobox; private boolean inUpdate = false; @@ -23,6 +25,9 @@ public class DecklistChooser extends JPanel chooseButton = new JButton("..."); chooseButton.setToolTipText("Select deck file..."); + generateButton = new JButton("Generate"); + generateButton.setToolTipText("Generate a new deck..."); + decklistCombobox = new JComboBox<>(); setupControls(); @@ -31,6 +36,7 @@ public class DecklistChooser extends JPanel private void setupControls() { chooseButton.addActionListener(e -> chooseFile()); + generateButton.addActionListener(e -> generateDeck()); decklistCombobox.setEditable(true); @@ -85,6 +91,9 @@ public class DecklistChooser extends JPanel gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; add(chooseButton, gbc); + + gbc.gridx = 2; + add(generateButton, gbc); } /** @@ -163,4 +172,15 @@ public class DecklistChooser extends JPanel } } } + + /** + * Generate a new deck using the “Generate Deck” dialog. + */ + private void generateDeck() { + String filename = DeckGenerator.generateDeck(); + if (filename != null) { + RecentDecklistUtil.putRecentDecklistFiles(filename); + update(); + } + } } diff --git a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java index 61061795a7c..2b9a3203d1e 100644 --- a/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/NewPlayerPanel.java @@ -63,7 +63,6 @@ public class NewPlayerPanel extends javax.swing.JPanel { public void showDeckElements(boolean show) { lblPlayerDeck.setVisible(show); decklist.setVisible(show); - btnGenerate.setVisible(show); } /** @@ -79,7 +78,6 @@ public class NewPlayerPanel extends javax.swing.JPanel { txtPlayerName = new javax.swing.JTextField(); lblPlayerDeck = new javax.swing.JLabel(); decklist = new DecklistChooser(); - btnGenerate = new javax.swing.JButton(); lblLevel = new javax.swing.JLabel(); spnLevel = new javax.swing.JSpinner(); @@ -87,9 +85,6 @@ public class NewPlayerPanel extends javax.swing.JPanel { lblPlayerDeck.setText("Deck:"); - btnGenerate.setText("Generate"); - btnGenerate.addActionListener(evt -> btnGenerateActionPerformed(evt)); - lblLevel.setText("Skill:"); spnLevel.setModel(new javax.swing.SpinnerNumberModel(2, 1, 10, 1)); @@ -110,9 +105,6 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addComponent(decklist, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addGroup(layout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, 87, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(layout.createSequentialGroup() .addComponent(lblLevel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) @@ -130,17 +122,11 @@ public class NewPlayerPanel extends javax.swing.JPanel { .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(lblPlayerDeck) - .addComponent(decklist, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(btnGenerate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(decklist, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) ); }// //GEN-END:initComponents - private void btnGenerateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnGenerateActionPerformed - generateDeck(); - }//GEN-LAST:event_btnGenerateActionPerformed - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton btnGenerate; private DecklistChooser decklist; private javax.swing.JLabel lblLevel; private javax.swing.JLabel lblPlayerDeck; From 4c78588846cbb91d2a3eed8251e4881533bbb675 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Tue, 30 Dec 2025 16:03:03 +0100 Subject: [PATCH 6/6] Replace GridBagLayout with BorderLayout --- .../client/components/DecklistChooser.java | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java index fe7515c889b..b1549ff95ab 100644 --- a/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java +++ b/Mage.Client/src/main/java/mage/client/components/DecklistChooser.java @@ -69,31 +69,22 @@ public class DecklistChooser extends JPanel int index, boolean isSelected, boolean cellHasFocus) { - JLabel c = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value != null) { - c.setToolTipText(value.toString()); - } + JLabel c = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value != null) { + c.setToolTipText(value.toString()); + } - return c; + return c; } }); - setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); + setLayout(new BorderLayout()); + add(decklistCombobox, BorderLayout.CENTER); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 1; - gbc.fill = GridBagConstraints.HORIZONTAL; - add(decklistCombobox, gbc); - - gbc.gridx = 1; - gbc.weightx = 0; - gbc.fill = GridBagConstraints.NONE; - add(chooseButton, gbc); - - gbc.gridx = 2; - add(generateButton, gbc); + JPanel rightPanel = new JPanel(new BorderLayout()); + rightPanel.add(chooseButton, BorderLayout.WEST); + rightPanel.add(generateButton, BorderLayout.EAST); + add(rightPanel, BorderLayout.EAST); } /**