diff --git a/forge-gui-desktop/src/main/java/forge/control/FControl.java b/forge-gui-desktop/src/main/java/forge/control/FControl.java index 6ec3e87cb73..c49f3b8ceff 100644 --- a/forge-gui-desktop/src/main/java/forge/control/FControl.java +++ b/forge-gui-desktop/src/main/java/forge/control/FControl.java @@ -60,6 +60,7 @@ import forge.model.FModel; import forge.sound.SoundSystem; import forge.screens.deckeditor.CDeckEditorUI; +import forge.screens.home.welcome.WelcomeWizardDialog; import forge.toolbox.FOptionPane; import forge.toolbox.FSkin; import forge.util.BuildInfo; @@ -299,6 +300,7 @@ public void componentMoved(final ComponentEvent e) { setGlobalKeyboardHandler(); FView.SINGLETON_INSTANCE.setSplashProgessBarMessage(getLocalizer().getMessage("lblOpeningMainWindow")); SwingUtilities.invokeLater(() -> Singletons.getView().initialize()); + SwingUtilities.invokeLater(WelcomeWizardDialog::maybeShow); } public boolean isSnapshot() { diff --git a/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java b/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java index d566dc6b094..66f5678dd2c 100644 --- a/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java +++ b/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java @@ -12,6 +12,7 @@ import forge.error.ExceptionHandler; import forge.localinstance.properties.ForgeConstants; +import forge.screens.home.welcome.WelcomeWizardDialog; import forge.toolbox.FOptionPane; import forge.util.BuildInfo; import forge.util.FileUtil; @@ -30,6 +31,7 @@ public static JMenu getMenu() { menu.add(getMenu_GettingStarted()); menu.add(getMenu_Troubleshooting()); menu.add(getMenuItem_KeyboardShortcuts()); + menu.add(getMenuItem_ReplayWelcomeWizard()); menu.addSeparator(); menu.add(getMenuItem_ReleaseNotes()); menu.add(getMenuItem_License()); @@ -77,6 +79,13 @@ private static JMenuItem getMenuItem_KeyboardShortcuts() { return menuItem; } + private static JMenuItem getMenuItem_ReplayWelcomeWizard() { + final Localizer localizer = Localizer.getInstance(); + JMenuItem menuItem = new JMenuItem(localizer.getMessage("btnReplayWelcomeWizard")); + menuItem.addActionListener(e -> WelcomeWizardDialog.replay()); + return menuItem; + } + private static JMenuItem getMenuItem_HowToPlayFile() { final Localizer localizer = Localizer.getInstance(); JMenuItem menuItem = new JMenuItem(localizer.getMessage("lblHowtoPlay")); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index 52f788ef821..f8f01258bb6 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -22,6 +22,7 @@ import forge.player.GamePlayerUtil; import forge.screens.deckeditor.CDeckEditorUI; import forge.screens.deckeditor.controllers.CEditorTokenViewer; +import forge.screens.home.welcome.WelcomeWizardDialog; import forge.sound.MusicPlaylist; import forge.sound.SoundSystem; import forge.toolbox.FComboBox; @@ -180,6 +181,8 @@ public void initialize() { view.getBtnReset().setCommand((UiCommand) CSubmenuPreferences.this::resetForgeSettingsToDefault); + view.getBtnReplayWelcomeWizard().setCommand((UiCommand) WelcomeWizardDialog::replay); + view.getBtnDeleteEditorUI().setCommand((UiCommand) CSubmenuPreferences.this::resetDeckEditorLayout); view.getBtnDeleteWorkshopUI().setCommand((UiCommand) CSubmenuPreferences.this::resetWorkshopLayout); @@ -351,21 +354,7 @@ private void initializeCloseActionComboBox() { } private void initializeDefaultLanguageComboBox() { - final File lang_root = new File(ForgeConstants.LANG_DIR); - final File[] files = lang_root.listFiles(); - final List allLanguages = new ArrayList<>(); - for ( File file : files ) { - if ( !file.isFile() ) { - continue; - } - String languageName = file.getName(); - if (!languageName.endsWith(".properties")) { - continue; - } - allLanguages.add(languageName.replace(".properties", "")); - } - final String [] choices = new String[ allLanguages.size() ]; - allLanguages.toArray( choices ); + final String[] choices = ForgeConstants.getAvailableLanguages().toArray(new String[0]); final FPref userSetting = FPref.UI_LANGUAGE; final FComboBoxPanel panel = this.view.getCbpDefaultLanguageComboBoxPanel(); final FComboBox comboBox = createComboBox(choices, userSetting); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index e6be6e2acf5..d4ba76a7044 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -52,6 +52,7 @@ public enum VSubmenuPreferences implements IVSubmenu { ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); private final FLabel btnReset = new FLabel.Builder().opaque(true).hoverable(true).text(localizer.getMessage("btnReset")).build(); + private final FLabel btnReplayWelcomeWizard = new FLabel.Builder().opaque(true).hoverable(true).text(localizer.getMessage("btnReplayWelcomeWizard")).build(); private final FLabel btnDeleteMatchUI = new FLabel.Builder().opaque(true).hoverable(true).text(localizer.getMessage("btnDeleteMatchUI")).build(); private final FLabel btnDeleteEditorUI = new FLabel.Builder().opaque(true).hoverable(true).text(localizer.getMessage("btnDeleteEditorUI")).build(); private final FLabel btnDeleteWorkshopUI = new FLabel.Builder().opaque(true).hoverable(true).text(localizer.getMessage("btnDeleteWorkshopUI")).build(); @@ -188,6 +189,8 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(btnContentDirectoryUI, twoButtonConstraints2); pnlPrefs.add(btnClearImageCache, twoButtonConstraints1); pnlPrefs.add(btnTokenPreviewer, twoButtonConstraints2); + pnlPrefs.add(btnReplayWelcomeWizard, twoButtonConstraints1); + pnlPrefs.add(new JLabel(""), twoButtonConstraints2); // Search bar pnlPrefs.add(getSearchPanel(), "w 80%!, h 28px!, gap 25px 0 30px 40px, span 2 1"); @@ -1066,6 +1069,10 @@ public FLabel getBtnReset() { return btnReset; } + public FLabel getBtnReplayWelcomeWizard() { + return btnReplayWelcomeWizard; + } + public FLabel getBtnPlayerName() { return btnPlayerName; } diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/welcome/OnboardingPresets.java b/forge-gui-desktop/src/main/java/forge/screens/home/welcome/OnboardingPresets.java new file mode 100644 index 00000000000..a57cb9e1074 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/welcome/OnboardingPresets.java @@ -0,0 +1,46 @@ +package forge.screens.home.welcome; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +import forge.localinstance.properties.ForgePreferences.FPref; + +public final class OnboardingPresets { + private OnboardingPresets() {} + + public static final Map AI_CASUAL = build( + FPref.UI_OVERLAY_DRAFT_RANKING, "true", + FPref.UI_ORDER_HAND, "true", + FPref.YIELD_AUTO_PASS_NO_ACTIONS, "true", + FPref.UI_REMIND_ON_PRIORITY, "true", + FPref.UI_SHOW_STORM_COUNT_IN_PROMPT, "true", + FPref.UI_DETAILED_SPELLDESC_IN_PROMPT, "true"); + public static final Map AI_EXPERT = build( + FPref.UI_OVERLAY_DRAFT_RANKING, "false", + FPref.UI_ORDER_HAND, "false", + FPref.YIELD_AUTO_PASS_NO_ACTIONS, "false", + FPref.UI_REMIND_ON_PRIORITY, "false", + FPref.UI_SHOW_STORM_COUNT_IN_PROMPT, "false", + FPref.UI_DETAILED_SPELLDESC_IN_PROMPT, "false"); + + public static final Map LAYOUT_DEFAULT = build( + FPref.UI_GROUP_PERMANENTS, "default"); + public static final Map LAYOUT_COMPACT = build( + FPref.UI_GROUP_PERMANENTS, "group_all"); + + public static final Map OVERLAYS_NONE = build( + FPref.UI_SHOW_CARD_OVERLAYS, "false", + FPref.UI_TARGETING_OVERLAY, "0"); + public static final Map OVERLAYS_ON = build( + FPref.UI_SHOW_CARD_OVERLAYS, "true", + FPref.UI_TARGETING_OVERLAY, "2"); + + private static Map build(Object... kvs) { + EnumMap m = new EnumMap<>(FPref.class); + for (int i = 0; i < kvs.length; i += 2) { + m.put((FPref) kvs[i], (String) kvs[i + 1]); + } + return Collections.unmodifiableMap(m); + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/welcome/WelcomeWizardDialog.java b/forge-gui-desktop/src/main/java/forge/screens/home/welcome/WelcomeWizardDialog.java new file mode 100644 index 00000000000..8f535f5b154 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/welcome/WelcomeWizardDialog.java @@ -0,0 +1,596 @@ +package forge.screens.home.welcome; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.LayoutManager; +import java.awt.Rectangle; +import java.awt.event.ItemEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.Scrollable; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.WindowConstants; +import javax.swing.border.Border; + +import forge.Singletons; +import forge.gui.framework.EDocID; +import forge.gui.framework.FScreen; +import forge.localinstance.properties.ForgeConstants; +import forge.localinstance.properties.ForgePreferences; +import forge.localinstance.properties.ForgePreferences.FPref; +import forge.menus.MenuUtil; +import forge.model.FModel; +import forge.screens.home.CHomeUI; +import forge.toolbox.FButton; +import forge.toolbox.FCheckBox; +import forge.toolbox.FComboBox; +import forge.toolbox.FLabel; +import forge.toolbox.FOptionPane; +import forge.toolbox.FRadioButton; +import forge.toolbox.FScrollPane; +import forge.toolbox.FSkin; +import forge.util.Localizer; +import forge.view.FDialog; +import net.miginfocom.swing.MigLayout; + +@SuppressWarnings("serial") +public final class WelcomeWizardDialog extends FDialog { + + public static void maybeShow() { + if (!FModel.getPreferences().getPrefBoolean(FPref.WELCOME_SHOWN)) { + new WelcomeWizardDialog().setVisible(true); + } + } + + public static void replay() { + new WelcomeWizardDialog().setVisible(true); + } + + private final Localizer L = Localizer.getInstance(); + private final ForgePreferences prefs = FModel.getPreferences(); + + private final CardLayout cardLayout = new CardLayout(); + private final JPanel cardHost; + private final List pages = new ArrayList<>(); + private int currentIndex = 0; + + private FButton btnBack; + private FButton btnNext; + private FCheckBox cbDontShowAgain; + private FComboBox cbLanguage; + private final String initialLanguage; + + private WelcomeWizardDialog() { + super(true, false, "10"); + setTitle(L.getMessage("lblWelcomeWizardTitle")); + setSize(640, 520); + setMinimumSize(new Dimension(560, 460)); + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + // Default FTitleBar separator uses CLR_BORDERS.stepColor(0), which reads lighter than the outer frame. + getTitleBar().setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, + FSkin.getColor(FSkin.Colors.CLR_BORDERS).getColor())); + initialLanguage = prefs.getPref(FPref.UI_LANGUAGE); + + cardHost = new JPanel(cardLayout); + cardHost.setOpaque(false); + + pages.add(buildSplashPage()); + pages.add(buildAiPage()); + pages.add(buildBattlefieldPage()); + pages.add(buildClosingPage()); + + for (int i = 0; i < pages.size(); i++) { + final FScrollPane scroll = new FScrollPane(pages.get(i).panel, false, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scroll.setBorder((Border) null); + scroll.setViewportBorder(null); + scroll.setOpaque(false); + scroll.getViewport().setOpaque(false); + cardHost.add(scroll, String.valueOf(i)); + } + + final JPanel root = new JPanel(new BorderLayout()); + root.setOpaque(false); + root.add(cardHost, BorderLayout.CENTER); + root.add(buildNavBar(), BorderLayout.SOUTH); + add(root, "w 100%!, h 100%!"); + + addWindowListener(new WindowAdapter() { + @Override public void windowClosing(final WindowEvent e) { onClose(); } + }); + + showPage(0); + } + + private WizardPage buildSplashPage() { + final JPanel p = newPagePanel(); + p.add(heading(L.getMessage("lblWelcomeWizardSplashHeading")), "w 100%, wrap, gapbottom 10"); + p.add(new FLabel.Builder() + .text("" + L.getMessage("lblWelcomeWizardSplashIntro") + "") + .fontSize(13).fontAlign(SwingConstants.LEFT).build(), + "w 100%, wrap, gapbottom 20"); + + final JPanel spacer = new JPanel(); + spacer.setOpaque(false); + p.add(spacer, "w 100%, growy, pushy, wrap"); + + cbLanguage = new FComboBox<>(ForgeConstants.getAvailableLanguages().toArray(new String[0])); + cbLanguage.setSelectedItem(initialLanguage); + p.add(new FLabel.Builder() + .text(L.getMessage("lblWelcomeWizardLanguagePageTitle") + ":") + .fontSize(13).fontAlign(SwingConstants.LEFT).build(), + "split 2, gapbottom 15"); + p.add(cbLanguage, "w 160!, h 26!, wrap, gapbottom 15"); + + cbDontShowAgain = new FCheckBox(L.getMessage("lblWelcomeWizardDontShowAgain")); + cbDontShowAgain.setSelected(true); + p.add(cbDontShowAgain, "w 100%, wrap"); + + return new WizardPage(p, Collections.emptyList()); + } + + private WizardPage buildAiPage() { + final JPanel p = newPagePanel(); + p.add(heading(L.getMessage("lblWelcomeWizardAiPageTitle")), "w 100%, wrap, gapbottom 5"); + p.add(noteLabel(L.getMessage("lblWelcomeWizardAiPageIntro")), "w 100%, wrap, gapbottom 15"); + + final LinkedHashMap> options = new LinkedHashMap<>(); + options.put(describe(L.getMessage("lblWelcomeWizardAiCasual"), L.getMessage("lblWelcomeWizardAiCasualDesc")), + OnboardingPresets.AI_CASUAL); + options.put(describe(L.getMessage("lblWelcomeWizardAiExpert"), L.getMessage("lblWelcomeWizardAiExpertDesc")), + OnboardingPresets.AI_EXPERT); + final RadioAxis axis = new RadioAxis(options, true); + axis.attachTo(p); + + final FLabel preview = previewLabel(); + p.add(previewPanel(preview), "w 100%, wrap, gaptop 15"); + axis.onChange = () -> preview.setText(renderPrefsHtml(axis.currentSelectionValues(prefs))); + + return new WizardPage(p, Collections.singletonList(axis)); + } + + private WizardPage buildBattlefieldPage() { + final JPanel p = newPagePanel(); + p.add(heading(L.getMessage("lblWelcomeWizardBfPageTitle")), "w 100%, wrap, gapbottom 5"); + p.add(noteLabel(L.getMessage("lblWelcomeWizardBfPageIntro")), "w 100%, wrap, gapbottom 15"); + + p.add(subheading(L.getMessage("lblWelcomeWizardBfLayout")), "w 100%, wrap, gapbottom 5"); + final LinkedHashMap> layoutOpts = new LinkedHashMap<>(); + layoutOpts.put(describe(L.getMessage("lblWelcomeWizardBfLayoutDefault"), L.getMessage("lblWelcomeWizardBfLayoutDefaultDesc")), + OnboardingPresets.LAYOUT_DEFAULT); + layoutOpts.put(describe(L.getMessage("lblWelcomeWizardBfLayoutCompact"), L.getMessage("lblWelcomeWizardBfLayoutCompactDesc")), + OnboardingPresets.LAYOUT_COMPACT); + final RadioAxis layoutAxis = new RadioAxis(layoutOpts, true); + layoutAxis.attachTo(p); + + p.add(subheading(L.getMessage("lblWelcomeWizardBfOverlays")), "w 100%, wrap, gaptop 15, gapbottom 5"); + final LinkedHashMap> overlayOpts = new LinkedHashMap<>(); + overlayOpts.put(describe(L.getMessage("lblWelcomeWizardBfOverlaysOn"), L.getMessage("lblWelcomeWizardBfOverlaysOnDesc")), + OnboardingPresets.OVERLAYS_ON); + overlayOpts.put(describe(L.getMessage("lblWelcomeWizardBfOverlaysNone"), L.getMessage("lblWelcomeWizardBfOverlaysNoneDesc")), + OnboardingPresets.OVERLAYS_NONE); + final RadioAxis overlayAxis = new RadioAxis(overlayOpts, true); + overlayAxis.attachTo(p); + + final FLabel preview = previewLabel(); + p.add(previewPanel(preview), "w 100%, wrap, gaptop 15"); + final Runnable refresh = () -> { + final EnumMap combined = new EnumMap<>(FPref.class); + combined.putAll(layoutAxis.currentSelectionValues(prefs)); + combined.putAll(overlayAxis.currentSelectionValues(prefs)); + preview.setText(renderPrefsHtml(combined)); + }; + layoutAxis.onChange = refresh; + overlayAxis.onChange = refresh; + + return new WizardPage(p, Arrays.asList(layoutAxis, overlayAxis)); + } + + private WizardPage buildClosingPage() { + final JPanel p = newPagePanel(); + p.add(heading(L.getMessage("lblWelcomeWizardClosingHeading")), "w 100%, wrap, gapbottom 10"); + p.add(new FLabel.Builder() + .text("" + L.getMessage("lblWelcomeWizardClosingIntro") + "") + .fontSize(13).fontAlign(SwingConstants.LEFT).build(), + "w 100%, wrap, gapbottom 15"); + + final FButton openPrefs = new FButton(L.getMessage("btnWelcomeWizardOpenPreferences")); + openPrefs.addActionListener(e -> openPreferencesScreen()); + p.add(openPrefs, "w 240!, h 30!, wrap, gapbottom 20"); + + p.add(new FLabel.Builder() + .text("" + L.getMessage("lblWelcomeWizardClosingMoreInfo") + "") + .fontSize(13).fontAlign(SwingConstants.LEFT).build(), + "w 100%, wrap, gapbottom 10"); + + final FButton btnWiki = new FButton(L.getMessage("lblWelcomeWizardWikiLink")); + btnWiki.addActionListener(e -> MenuUtil.openUrlInBrowser(ForgeConstants.GITHUB_FORGE_URL + "wiki")); + final FButton btnDiscord = new FButton(L.getMessage("lblWelcomeWizardDiscordLink")); + btnDiscord.addActionListener(e -> MenuUtil.openUrlInBrowser("https://discord.gg/HcPJNyD66a")); + + final JPanel linkRow = new JPanel(new MigLayout("insets 0, gap 15")); + linkRow.setOpaque(false); + linkRow.add(btnWiki, "w 240!, h 30!"); + linkRow.add(btnDiscord, "w 240!, h 30!"); + p.add(linkRow, "w 100%, wrap"); + + return new WizardPage(p, Collections.emptyList()); + } + + private void openPreferencesScreen() { + // Dispose before switching screens so the prefs screen comes to the foreground. + writeWelcomeShownFromCheckbox(); + prefs.save(); + dispose(); + Singletons.getControl().setCurrentScreen(FScreen.HOME_SCREEN); + CHomeUI.SINGLETON_INSTANCE.itemClick(EDocID.HOME_PREFERENCES); + } + + private JPanel buildNavBar() { + final JPanel bar = new JPanel(new MigLayout("insets 10 10 20 10, gap 10, ax right")); + bar.setOpaque(false); + + btnBack = new FButton(L.getMessage("lblWelcomeWizardBack")); + btnBack.addActionListener(e -> onBack()); + + btnNext = new FButton(L.getMessage("lblWelcomeWizardNext")); + btnNext.addActionListener(e -> onNext()); + + bar.add(btnBack, "w 120!, h 30!"); + bar.add(btnNext, "w 120!, h 30!"); + return bar; + } + + private void showPage(final int index) { + currentIndex = index; + cardLayout.show(cardHost, String.valueOf(index)); + pages.get(index).onEnter(prefs); + btnBack.setEnabled(index > 0); + btnNext.setText(index == pages.size() - 1 + ? L.getMessage("lblWelcomeWizardFinish") + : L.getMessage("lblWelcomeWizardNext")); + // CardLayout swaps repaint children only; force the dialog repaint so FDialog.paint() redraws the rounded outer border. + repaint(); + } + + private void onBack() { + if (currentIndex > 0) showPage(currentIndex - 1); + } + + private void onNext() { + if (!commitCurrentPage()) return; + if (currentIndex < pages.size() - 1) { + showPage(currentIndex + 1); + } else { + onFinish(); + } + } + + /** Returns false if the user cancelled the override-warning and we should stay on this page. */ + private boolean commitCurrentPage() { + final WizardPage page = pages.get(currentIndex); + if (page.requiresOverrideConfirm() + && !FOptionPane.showConfirmDialog( + L.getMessage("msgWelcomeWizardOverrideCustom"), + L.getMessage("lblWelcomeWizardOverrideCustomTitle"))) { + return false; + } + page.applyPending(prefs); + if (currentIndex == 0) { + final String selected = (String) cbLanguage.getSelectedItem(); + if (selected != null && !selected.equals(prefs.getPref(FPref.UI_LANGUAGE))) { + prefs.setPref(FPref.UI_LANGUAGE, selected); + } + } + prefs.save(); + return true; + } + + private void onFinish() { + final boolean languageChanged = !Objects.equals(initialLanguage, prefs.getPref(FPref.UI_LANGUAGE)); + if (languageChanged && FOptionPane.showConfirmDialog( + L.getMessage("msgWelcomeWizardRestart"), + L.getMessage("lblWelcomeWizardRestartTitle"), + L.getMessage("lblYes"), + L.getMessage("lblWelcomeWizardLater"))) { + // Re-arm the wizard for next launch so the user sees it in the new language regardless of the splash checkbox. + prefs.setPref(FPref.WELCOME_SHOWN, "false"); + prefs.save(); + dispose(); + Singletons.getControl().restartForge(); + } else { + writeWelcomeShownFromCheckbox(); + prefs.save(); + dispose(); + } + } + + private void onClose() { + if (!commitCurrentPage()) return; + writeWelcomeShownFromCheckbox(); + prefs.save(); + dispose(); + } + + private void writeWelcomeShownFromCheckbox() { + prefs.setPref(FPref.WELCOME_SHOWN, String.valueOf(cbDontShowAgain.isSelected())); + } + + private static JPanel newPagePanel() { + final JPanel p = new ScrollableWidthPanel(new MigLayout("insets 20, gap 0, wrap 1, fillx")); + p.setOpaque(false); + return p; + } + + /** JPanel that tracks the viewport width (so MigLayout doesn't overflow horizontally), + * and tracks the viewport height when its preferred height fits (so MigLayout's + * growy/pushy spacers can fill the page). When content exceeds the viewport, falls + * back to preferred height so the scrollpane shows a vertical scrollbar. */ + @SuppressWarnings("serial") + private static final class ScrollableWidthPanel extends JPanel implements Scrollable { + ScrollableWidthPanel(final LayoutManager layout) { super(layout); } + @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } + @Override public int getScrollableUnitIncrement(final Rectangle r, final int o, final int d) { return 16; } + @Override public int getScrollableBlockIncrement(final Rectangle r, final int o, final int d) { return 64; } + @Override public boolean getScrollableTracksViewportWidth() { return true; } + @Override public boolean getScrollableTracksViewportHeight() { + return getParent() != null && getPreferredSize().height <= getParent().getHeight(); + } + } + + private static FLabel heading(final String text) { + return new FLabel.Builder().text(text).fontSize(20).fontStyle(Font.BOLD) + .fontAlign(SwingConstants.LEFT).build(); + } + + private static FLabel subheading(final String text) { + return new FLabel.Builder().text(text).fontSize(15).fontStyle(Font.BOLD) + .fontAlign(SwingConstants.LEFT).build(); + } + + private static FLabel noteLabel(final String text) { + return new FLabel.Builder().text("" + text + "").fontSize(12) + .fontStyle(Font.ITALIC).fontAlign(SwingConstants.LEFT).build(); + } + + private static String describe(final String name, final String desc) { + return "" + name + "
" + desc + ""; + } + + private FLabel previewLabel() { + return new FLabel.Builder().text("").fontSize(12).fontAlign(SwingConstants.LEFT).build(); + } + + private JComponent previewPanel(final FLabel previewLabel) { + final JPanel box = new JPanel(new MigLayout("insets 10, fillx")); + box.setOpaque(false); + // Hardcoded high-contrast grey: FSkin's CLR_BORDERS reads too dark here and skin-aware variants leave the right edge invisible. + box.setBorder(BorderFactory.createLineBorder(new Color(180, 180, 200), 2)); + box.add(previewLabel, "growx, wrap"); + return box; + } + + private String renderPrefsHtml(final Map map) { + final StringBuilder sb = new StringBuilder(""); + sb.append(L.getMessage("lblWelcomeWizardPreviewHeading")); + sb.append("

"); + for (final Map.Entry e : map.entrySet()) { + sb.append(""); + sb.append(""); + sb.append(""); + } + sb.append("
").append(friendlyPrefName(e.getKey())).append(":").append(friendlyValue(e.getKey(), e.getValue())).append("
"); + return sb.toString(); + } + + private String friendlyPrefName(final FPref key) { + switch (key) { + case UI_OVERLAY_DRAFT_RANKING: return L.getMessage("lblShowDraftRankingOverlay"); + case UI_ORDER_HAND: return L.getMessage("cbOrderHand"); + case YIELD_AUTO_PASS_NO_ACTIONS: return L.getMessage("lblEnableAutoPass"); + case UI_REMIND_ON_PRIORITY: return L.getMessage("cbRemindOnPriority"); + case UI_SHOW_STORM_COUNT_IN_PROMPT: return L.getMessage("cbShowStormCount"); + case UI_DETAILED_SPELLDESC_IN_PROMPT: return L.getMessage("cbDetailedPaymentDesc"); + case UI_TARGETING_OVERLAY: return L.getMessage("lblTargetingArcs"); + case UI_GROUP_PERMANENTS: return L.getMessage("cbpStackGroupPermanents"); + case UI_SHOW_CARD_OVERLAYS: return L.getMessage("lblShowCardOverlays"); + default: return key.name(); + } + } + + private String friendlyValue(final FPref key, final String value) { + if (key == FPref.UI_TARGETING_OVERLAY) { + if ("0".equals(value)) return L.getMessage("lblOff"); + if ("1".equals(value)) return L.getMessage("lblCardMouseOver"); + if ("2".equals(value)) return L.getMessage("lblAlwaysOn"); + } + if (key == FPref.UI_GROUP_PERMANENTS) { + if ("default".equals(value)) return L.getMessage("lblGroupDefault"); + if ("stack".equals(value)) return L.getMessage("lblGroupStack"); + if ("group_creatures".equals(value)) return L.getMessage("lblGroupCreatures"); + if ("group_all".equals(value)) return L.getMessage("lblGroupAll"); + } + if ("true".equals(value)) return L.getMessage("lblYes"); + if ("false".equals(value)) return L.getMessage("lblNo"); + return value; + } + + private static final class WizardPage { + final JPanel panel; + final List axes; + + WizardPage(final JPanel panel, final List axes) { + this.panel = panel; + this.axes = axes; + } + + void onEnter(final ForgePreferences prefs) { + for (final RadioAxis axis : axes) axis.recomputePreselection(prefs); + } + + boolean requiresOverrideConfirm() { + for (final RadioAxis axis : axes) { + if (axis.isOverridingCustom()) return true; + } + return false; + } + + /** Returns true if any FPref was written. */ + boolean applyPending(final ForgePreferences prefs) { + boolean wrote = false; + for (final RadioAxis axis : axes) { + if (axis.applyPending(prefs)) wrote = true; + } + return wrote; + } + } + + private static final class RadioAxis { + final LinkedHashMap> options; + final boolean offerKeepCurrent; + final ButtonGroup group = new ButtonGroup(); + final List buttons = new ArrayList<>(); + final List> presetForButton = new ArrayList<>(); + JPanel ownPanel; // each axis owns its own container so dynamic keep-current insertion is local + FRadioButton keepCurrentButton; // null when not currently shown + int preselectedIndex = -1; + Runnable onChange; // fired when the user picks a different option + + RadioAxis(final LinkedHashMap> options, + final boolean offerKeepCurrent) { + this.options = options; + this.offerKeepCurrent = offerKeepCurrent; + } + + void attachTo(final JPanel parent) { + ownPanel = new JPanel(new MigLayout("insets 0, gap 0, wrap 1, fillx")); + ownPanel.setOpaque(false); + for (final Map.Entry> e : options.entrySet()) { + final FRadioButton b = new FRadioButton(e.getKey()); + group.add(b); + buttons.add(b); + presetForButton.add(e.getValue()); + wireChangeListener(b); + ownPanel.add(b, "w 100%, wrap"); + } + parent.add(ownPanel, "w 100%, wrap"); + } + + private void wireChangeListener(final FRadioButton b) { + b.addItemListener(e -> { + if (e.getStateChange() == ItemEvent.SELECTED && onChange != null) onChange.run(); + }); + } + + Set prefUnion() { + final EnumSet result = EnumSet.noneOf(FPref.class); + for (final Map preset : presetForButton) result.addAll(preset.keySet()); + return result; + } + + /** What the page's preference state would be if the user committed right now. + * For a real preset selection: that preset's map. + * For Keep custom settings (or nothing selected): current FPref values for the pref-union. */ + Map currentSelectionValues(final ForgePreferences prefs) { + final int idx = selectedPresetIndex(); + if (idx >= 0) return presetForButton.get(idx); + final Map result = new EnumMap<>(FPref.class); + for (final FPref key : prefUnion()) result.put(key, prefs.getPref(key)); + return result; + } + + void recomputePreselection(final ForgePreferences prefs) { + int bestIdx = -1; + int bestSize = -1; + for (int i = 0; i < presetForButton.size(); i++) { + final Map preset = presetForButton.get(i); + if (matches(preset, prefs) && preset.size() > bestSize) { + bestIdx = i; + bestSize = preset.size(); + } + } + + if (bestIdx >= 0) { + removeKeepCurrentButton(); + buttons.get(bestIdx).setSelected(true); + preselectedIndex = bestIdx; + } else if (offerKeepCurrent) { + addKeepCurrentButton(); + keepCurrentButton.setSelected(true); + preselectedIndex = -1; + } else if (!buttons.isEmpty()) { + buttons.get(0).setSelected(true); + preselectedIndex = 0; + } + } + + boolean isOverridingCustom() { + return preselectedIndex == -1 && (keepCurrentButton == null || !keepCurrentButton.isSelected()) + && selectedPresetIndex() >= 0; + } + + boolean applyPending(final ForgePreferences prefs) { + final int idx = selectedPresetIndex(); + if (idx < 0 || idx == preselectedIndex) return false; + for (final Map.Entry e : presetForButton.get(idx).entrySet()) { + prefs.setPref(e.getKey(), e.getValue()); + } + return true; + } + + private int selectedPresetIndex() { + for (int i = 0; i < buttons.size(); i++) { + if (buttons.get(i).isSelected()) return i; + } + return -1; + } + + private boolean matches(final Map preset, final ForgePreferences prefs) { + for (final Map.Entry e : preset.entrySet()) { + if (!e.getValue().equals(prefs.getPref(e.getKey()))) return false; + } + return true; + } + + private void addKeepCurrentButton() { + if (keepCurrentButton != null || ownPanel == null) return; + keepCurrentButton = new FRadioButton("" + + Localizer.getInstance().getMessage("lblWelcomeWizardKeepCurrent") + ""); + group.add(keepCurrentButton); + wireChangeListener(keepCurrentButton); + ownPanel.add(keepCurrentButton, "w 100%, wrap"); + ownPanel.revalidate(); + } + + private void removeKeepCurrentButton() { + if (keepCurrentButton == null) return; + group.remove(keepCurrentButton); + if (ownPanel != null) { + ownPanel.remove(keepCurrentButton); + ownPanel.revalidate(); + ownPanel.repaint(); + } + keepCurrentButton = null; + } + } +} diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 3ef3b9d9afc..a34a346f054 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3532,3 +3532,46 @@ lblCommanderBracketReasonExtraTurnsTwo=2 Extra Turn cards make the deck at least lblCommanderBracketReasonExtraTurnsFew=Fewer than 2 Extra Turn cards do not raise the bracket. lblCommanderBracketReasonLateGameCombo=Late Game 2-card combos make the deck at least Bracket 3. lblCommanderBracketReasonEarlyGameCombo=Early Game 2-card combos make the deck at least Bracket 4. + +# Welcome wizard +lblWelcomeWizardTitle=Welcome to Forge! +lblWelcomeWizardSplashHeading=Welcome to Forge! +lblWelcomeWizardSplashIntro=Forge is a free, open-source Magic: The Gathering rules engine maintained by volunteers. This short wizard sets up a few common preferences so you can dive in. You can change anything later from the Preferences screen, or replay this wizard from the Help menu. +lblWelcomeWizardWikiLink=Forge User Guide +lblWelcomeWizardDiscordLink=Forge Discord +lblWelcomeWizardClosingHeading=All set! +lblWelcomeWizardClosingIntro=Forge is ready to play. You can fine-tune any of these choices — and many more — from the Preferences screen at any time. +lblWelcomeWizardClosingMoreInfo=For more help see the Forge User Guide or join the Forge Discord to ask questions, share feedback, and chat with other players. +btnWelcomeWizardOpenPreferences=Open Preferences +lblWelcomeWizardDontShowAgain=Don''t show me this again +lblWelcomeWizardBack=Back +lblWelcomeWizardNext=Next +lblWelcomeWizardFinish=Finish +lblWelcomeWizardLanguagePageTitle=Language +lblWelcomeWizardLanguagePageIntro=Pick the language Forge uses. +lblWelcomeWizardAiPageTitle=Assistance level +lblWelcomeWizardAiPageIntro=Choose how much help the interface offers during a match. +lblWelcomeWizardAiCasual=Casual (Recommended) +lblWelcomeWizardAiCasualDesc=More automated guidance and hints during play. +lblWelcomeWizardAiExpert=Expert +lblWelcomeWizardAiExpertDesc=No automated guidance — closer to playing paper Magic. +lblWelcomeWizardBfPageTitle=Battlefield display +lblWelcomeWizardBfPageIntro=Tune how the battlefield is laid out and how much detail cards show. +lblWelcomeWizardBfLayout=Battlefield layout +lblWelcomeWizardBfLayoutDefault=Default +lblWelcomeWizardBfLayoutDefaultDesc=Forge''s default battlefield arrangement. +lblWelcomeWizardBfLayoutCompact=Compact +lblWelcomeWizardBfLayoutCompactDesc=Save screen space by grouping permanents together. +lblWelcomeWizardBfOverlays=Visual hints +lblWelcomeWizardBfOverlaysNone=None +lblWelcomeWizardBfOverlaysNoneDesc=A clean, paper-Magic feel — read the cards yourself, no on-screen hints. +lblWelcomeWizardBfOverlaysOn=On (Recommended) +lblWelcomeWizardBfOverlaysOnDesc=Helpful visual cues during play — hints on cards and arrows showing what targets what. +lblWelcomeWizardKeepCurrent=Keep custom settings +lblWelcomeWizardPreviewHeading=Preferences for this selection: +lblWelcomeWizardOverrideCustomTitle=Override custom preferences? +msgWelcomeWizardOverrideCustom=Continuing will replace your current settings. +lblWelcomeWizardRestartTitle=Restart Forge? +msgWelcomeWizardRestart=Restart Forge now to apply the language change? +lblWelcomeWizardLater=Later +btnReplayWelcomeWizard=Replay Welcome Wizard diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java index 92b8c68a181..15066621d06 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java @@ -22,7 +22,9 @@ import forge.util.Localizer; import java.io.File; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; public final class ForgeConstants { @@ -392,6 +394,24 @@ public static Map getUPnPPreferenceMapping() { ); } + /** Locale codes for each {@code .properties} file under {@link #LANG_DIR}, sorted alphabetically. + * Falls back to a single {@code en-US} entry if the directory is missing or empty. */ + public static List getAvailableLanguages() { + final List result = new ArrayList<>(); + final File[] files = new File(LANG_DIR).listFiles(); + if (files != null) { + for (final File f : files) { + final String name = f.getName(); + if (f.isFile() && name.endsWith(".properties")) { + result.add(name.replace(".properties", "")); + } + } + } + if (result.isEmpty()) result.add("en-US"); + Collections.sort(result); + return result; + } + public enum CounterDisplayLocation { TOP("Top of Card"), BOTTOM("Bottom of Card"); diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index 10b61e380c1..db57ba49d3f 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -330,7 +330,9 @@ public enum FPref implements PreferencesStore.IPref { SHORTCUT_PANELTABS("17 84"), SHORTCUT_CARDOVERLAYS("17 79"), - LAST_IMPORTED_CUBE_ID(""); + LAST_IMPORTED_CUBE_ID(""), + + WELCOME_SHOWN("false"); private final String strDefaultVal;