diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java b/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java index 6f66db4e75c..55dd5f501fa 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.java @@ -5,9 +5,7 @@ import java.awt.datatransfer.StringSelection; import java.net.BindException; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import javax.swing.JMenu; @@ -20,7 +18,6 @@ import forge.gamemodes.net.ChatMessage; import forge.gamemodes.net.NetConnectUtil; -import forge.gamemodes.net.server.FServerManager; import forge.gui.FNetOverlay; import forge.gui.FThreads; import forge.gui.SOverlayUtils; @@ -29,10 +26,8 @@ import forge.gui.framework.ICDoc; import forge.gui.util.SOptionPane; import forge.localinstance.properties.ForgeConstants; -import forge.localinstance.properties.ForgeNetPreferences; import forge.menus.IMenuProvider; import forge.menus.MenuUtil; -import forge.model.FModel; import forge.screens.home.CHomeUI; import forge.screens.home.CLobby; import forge.screens.home.VLobby; @@ -96,37 +91,11 @@ private void host() { } static void showServerAddressesDialog() { - final ForgeNetPreferences netPrefs = FModel.getNetPreferences(); - final int port = netPrefs.getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT); - final LinkedHashMap addresses = FServerManager.getAllLocalAddresses(); - final String externalAddress = FServerManager.getExternalAddress(); final Localizer localizer = Localizer.getInstance(); + final NetConnectUtil.ServerAddressList addresses = NetConnectUtil.collectHostedServerAddresses(); - // Collect rows in display order; we auto-copy and star whichever row matches - // the last-copied URL, falling back to the first row. - final List orderedLabels = new ArrayList<>(); - final List orderedUrls = new ArrayList<>(); - if (externalAddress != null) { - orderedLabels.add("External (WAN)"); - orderedUrls.add(externalAddress + ":" + port); - } - for (final Map.Entry entry : addresses.entrySet()) { - orderedLabels.add(entry.getKey()); - orderedUrls.add(entry.getValue() + ":" + port); - } - - // If the remembered URL is present in the current list, auto-copy and star it. - // Otherwise fall back to the first entry (external if present, else first local - // interface) — matches the old copyHostedServerUrl default. Do NOT overwrite - // the remembered value on fallback, so a later reconnect to the original - // network restores the preference. - final String rememberedUrl = netPrefs.getPref(ForgeNetPreferences.FNetPref.NET_LAST_COPIED_URL); - int starIndex = orderedUrls.indexOf(rememberedUrl); - if (starIndex < 0) { - starIndex = orderedUrls.isEmpty() ? -1 : 0; - } - if (starIndex >= 0) { - copyToClipboard(orderedUrls.get(starIndex)); + if (addresses.starIndex >= 0) { + copyToClipboard(addresses.urls.get(addresses.starIndex)); } final JPanel panel = new JPanel(new MigLayout("insets 0, gap 4 6, wrap 3", "[pref]30[pref]30[pref]")); @@ -142,25 +111,24 @@ static void showServerAddressesDialog() { panel.add(new FLabel.Builder().text("").build()); final FOptionPane[] holder = new FOptionPane[1]; - for (int i = 0; i < orderedUrls.size(); i++) { - final String url = orderedUrls.get(i); - final String label = (i == starIndex) ? orderedLabels.get(i) + " \u2605" : orderedLabels.get(i); + for (int i = 0; i < addresses.urls.size(); i++) { + final String url = addresses.urls.get(i); + final String label = (i == addresses.starIndex) ? addresses.labels.get(i) + " \u2605" : addresses.labels.get(i); panel.add(new FLabel.Builder().text(label).fontSize(12).fontAlign(SwingConstants.LEFT).build(), "growx"); panel.add(new FLabel.Builder().text(url).fontSize(12).fontAlign(SwingConstants.LEFT).build(), "growx"); final FButton btnCopy = new FButton(localizer.getMessage("lblCopy")); btnCopy.setFont(FSkin.getFont(11)); btnCopy.addActionListener(e -> { copyToClipboard(url); - netPrefs.setPref(ForgeNetPreferences.FNetPref.NET_LAST_COPIED_URL, url); - netPrefs.save(); + NetConnectUtil.rememberCopiedServerUrl(url); holder[0].setVisible(false); }); panel.add(btnCopy, "w 70!, h 24!"); } - if (starIndex >= 0) { + if (addresses.starIndex >= 0) { panel.add(new FLabel.Builder() - .text(localizer.getMessage("lblServerUrlCopiedToClipboard", orderedUrls.get(starIndex))) + .text(localizer.getMessage("lblServerUrlCopiedToClipboard", addresses.urls.get(addresses.starIndex))) .fontSize(11).fontStyle(Font.ITALIC).fontAlign(SwingConstants.LEFT).build(), "span 3, growx, gaptop 10"); } diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java index 5e5f738dc6e..f8a35319290 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineLobbyScreen.java @@ -203,7 +203,10 @@ private void activateHost() { FThreads.invokeInBackgroundThread(() -> { result[0] = NetConnectUtil.host(OnlineLobbyScreen.this, chatInterface); chatInterface.addMessage(result[0]); - NetConnectUtil.copyHostedServerUrl(); + FThreads.invokeInEdtLater(() -> { + OnlineScreen.Lobby.update(); + ServerAddressesDialog.show(); + }); }); OnlineScreen.Lobby.update(); }); diff --git a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java index 205a54ba4a1..270dd1d56a6 100644 --- a/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java +++ b/forge-gui-mobile/src/forge/screens/online/OnlineMenu.java @@ -16,36 +16,22 @@ public class OnlineMenu extends FPopupMenu { public enum OnlineScreen { - Lobby("lblPlayOnline", FSkinImage.FAVICON, OnlineLobbyScreen.class), - Chat("lblChat", FSkinImage.QUEST_NOTES, OnlineChatScreen.class), - Disconnect("lblDisconnect", FSkinImage.DELETE, null); + Lobby("lblPlayOnline", FSkinImage.FAVICON, OnlineLobbyScreen.class, null), + Chat("lblChat", FSkinImage.QUEST_NOTES, OnlineChatScreen.class, null), + ServerUrl("lblServerURL", FSkinImage.INFORMATION, null, ServerAddressesDialog::show), + Disconnect("lblDisconnect", FSkinImage.DELETE, null, OnlineMenu::handleDisconnect); private final FMenuItem item; private final Class screenClass; + private final Runnable customAction; private FScreen screen; - OnlineScreen(final String caption0, final FImage icon0, final Class screenClass0) { + OnlineScreen(final String caption0, final FImage icon0, final Class screenClass0, final Runnable customAction0) { screenClass = screenClass0; + customAction = customAction0; item = new FMenuItem(Forge.getLocalizer().getMessage(caption0), icon0, e -> { - if(screenClass == null) { - FOptionPane.showConfirmDialog( - Forge.getLocalizer().getMessage("lblLeaveLobbyDescription"), - Forge.getLocalizer().getMessage("lblDisconnect"), result -> { - if (result) { - if (FServerManager.getInstance() != null) - if(FServerManager.getInstance().isHosting()) { - FServerManager.getInstance().unsetReady(); - FServerManager.getInstance().stopServer(); - } - - if (OnlineLobbyScreen.getfGameClient() != null) - OnlineLobbyScreen.closeClient(); - - Forge.back(); - screen = null; - OnlineLobbyScreen.clearGameLobby(); - } - }); + if (customAction != null) { + customAction.run(); return; } Forge.back(); //remove current screen from chain @@ -85,6 +71,7 @@ public FScreen getScreen() { public void update(){ Disconnect.item.setEnabled(getGameLobby() != null); + ServerUrl.item.setEnabled(FServerManager.getInstance() != null && FServerManager.getInstance().isHosting()); } } @@ -121,6 +108,27 @@ public static OnlineMenu getMenu() { private OnlineMenu() { } + private static void handleDisconnect() { + FOptionPane.showConfirmDialog( + Forge.getLocalizer().getMessage("lblLeaveLobbyDescription"), + Forge.getLocalizer().getMessage("lblDisconnect"), result -> { + if (result) { + if (FServerManager.getInstance() != null) + if (FServerManager.getInstance().isHosting()) { + FServerManager.getInstance().unsetReady(); + FServerManager.getInstance().stopServer(); + } + + if (OnlineLobbyScreen.getfGameClient() != null) + OnlineLobbyScreen.closeClient(); + + Forge.back(); + OnlineScreen.Disconnect.screen = null; + OnlineLobbyScreen.clearGameLobby(); + } + }); + } + @Override protected void buildMenu() { FScreen currentScreen = Forge.getCurrentScreen(); diff --git a/forge-gui-mobile/src/forge/screens/online/ServerAddressesDialog.java b/forge-gui-mobile/src/forge/screens/online/ServerAddressesDialog.java new file mode 100644 index 00000000000..fb7b5ec02d6 --- /dev/null +++ b/forge-gui-mobile/src/forge/screens/online/ServerAddressesDialog.java @@ -0,0 +1,154 @@ +package forge.screens.online; + +import com.badlogic.gdx.utils.Align; +import com.google.common.collect.ImmutableList; + +import forge.Forge; +import forge.assets.FSkinColor; +import forge.assets.FSkinFont; +import forge.assets.FSkinImage; +import forge.gamemodes.net.NetConnectUtil; +import forge.gui.GuiBase; +import forge.toolbox.FButton; +import forge.toolbox.FLabel; +import forge.toolbox.FOptionPane; +import forge.toolbox.FScrollPane; +import forge.toolbox.FTextArea; +import forge.util.Localizer; +import forge.util.Utils; + +/** + * Mobile equivalent of the desktop "Server URL" dialog. Lists every reachable address + * (External + each local interface) with its own Copy button. Auto-copies and stars + * the previously remembered URL so repeat hosts on the same network don't need to + * pick the same row each time. + */ +public final class ServerAddressesDialog { + private ServerAddressesDialog() { } + + private static final float SIDE_PADDING = Utils.scale(10); + private static final float ROW_GAP = Utils.scale(8); + private static final float ROW_PADDING = Utils.scale(10); + private static final float COPY_BTN_WIDTH = Utils.AVG_FINGER_WIDTH * 1.1f; + private static final float COPY_BTN_HEIGHT = Utils.AVG_FINGER_HEIGHT * 0.9f; + + public static void show() { + final Localizer localizer = Localizer.getInstance(); + final NetConnectUtil.ServerAddressList addresses = NetConnectUtil.collectHostedServerAddresses(); + if (addresses.starIndex >= 0) { + GuiBase.getInterface().copyToClipboard(addresses.urls.get(addresses.starIndex)); + } + + final FOptionPane[] holder = new FOptionPane[1]; + final AddressList list = new AddressList(addresses, holder, localizer); + holder[0] = new FOptionPane( + localizer.getMessage("lblChooseAddressToCopy"), + FSkinFont.get(12), + localizer.getMessage("lblServerURL"), + FOptionPane.INFORMATION_ICON, + list, + ImmutableList.of(localizer.getMessage("lblOK")), + 0, + null); + holder[0].show(); + } + + private static final class AddressList extends FScrollPane { + private final FLabel[] labels; + private final FLabel[] urls; + private final FButton[] copyButtons; + private final FTextArea footer; + private final FSkinFont labelFont = FSkinFont.get(12); + private final FSkinFont urlFont = FSkinFont.get(14); + + AddressList(final NetConnectUtil.ServerAddressList addresses, final FOptionPane[] holder, final Localizer localizer) { + final int n = addresses.urls.size(); + labels = new FLabel[n]; + urls = new FLabel[n]; + copyButtons = new FButton[n]; + + for (int i = 0; i < n; i++) { + final String url = addresses.urls.get(i); + final FLabel.Builder labelBuilder = new FLabel.Builder() + .text(addresses.labels.get(i)) + .font(labelFont) + .align(Align.left) + .textColor(FSkinColor.get(FSkinColor.Colors.CLR_TEXT)); + if (i == addresses.starIndex) { + labelBuilder.icon(FSkinImage.STAR_FILLED).iconScaleFactor(1f); + } + labels[i] = add(labelBuilder.build()); + urls[i] = add(new FLabel.Builder() + .text(url) + .font(urlFont) + .align(Align.left) + .textColor(FSkinColor.get(FSkinColor.Colors.CLR_TEXT)) + .build()); + final FButton btnCopy = new FButton(localizer.getMessage("lblCopy")); + btnCopy.setCommand(e -> { + GuiBase.getInterface().copyToClipboard(url); + NetConnectUtil.rememberCopiedServerUrl(url); + if (holder[0] != null) { + holder[0].hide(); + } + }); + copyButtons[i] = add(btnCopy); + } + + if (addresses.starIndex >= 0) { + footer = new FTextArea(false, + localizer.getMessage("lblServerUrlCopiedToClipboard", addresses.urls.get(addresses.starIndex))); + footer.setFont(FSkinFont.get(11)); + add(footer); + } else { + footer = null; + } + + final float contentWidth = Forge.getScreenWidth() - 2 * FOptionPane.PADDING - 2 * SIDE_PADDING; + // Reserve some headroom for the icon + prompt + buttons that sit above and below the scroll viewport. + final float maxVisibleHeight = FOptionPane.getMaxDisplayObjHeight() * 0.85f; + setHeight(Math.min(computeFullHeight(contentWidth), maxVisibleHeight)); + } + + private float computeFullHeight(final float contentWidth) { + final float rowTextHeight = labelFont.getLineHeight() + urlFont.getLineHeight(); + final float rowHeight = Math.max(rowTextHeight, COPY_BTN_HEIGHT) + 2 * ROW_PADDING; + float h = rowHeight * labels.length; + if (footer != null) { + h += ROW_GAP + footer.getPreferredHeight(contentWidth) + SIDE_PADDING; + } + return h; + } + + @Override + protected ScrollBounds layoutAndGetScrollBounds(final float visibleWidth, final float visibleHeight) { + final float contentX = SIDE_PADDING; + final float contentWidth = visibleWidth - 2 * SIDE_PADDING; + float y = 0; + + final float rowTextHeight = labelFont.getLineHeight() + urlFont.getLineHeight(); + final float rowHeight = Math.max(rowTextHeight, COPY_BTN_HEIGHT) + 2 * ROW_PADDING; + final float btnWidth = Math.min(COPY_BTN_WIDTH, contentWidth * 0.25f); + final float textX = contentX + btnWidth + 2 * ROW_PADDING; + final float textWidth = contentWidth - btnWidth - 2 * ROW_PADDING; + + for (int i = 0; i < labels.length; i++) { + final float rowTop = y + ROW_PADDING; + final float labelHeight = labelFont.getLineHeight(); + final float urlHeight = urlFont.getLineHeight(); + copyButtons[i].setBounds(contentX, y + (rowHeight - COPY_BTN_HEIGHT) / 2, btnWidth, COPY_BTN_HEIGHT); + labels[i].setBounds(textX, rowTop, textWidth, labelHeight); + urls[i].setBounds(textX, rowTop + labelHeight, textWidth, urlHeight); + y += rowHeight; + } + + if (footer != null) { + y += ROW_GAP; + footer.setBounds(contentX, y, contentWidth, footer.getPreferredHeight(contentWidth)); + y += footer.getHeight() + SIDE_PADDING; + } + + return new ScrollBounds(visibleWidth, y); + } + } +} diff --git a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java index 4cc06a4f1fb..8348511645f 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java @@ -24,6 +24,7 @@ import forge.util.URLValidator; import org.apache.commons.lang3.StringUtils; +import java.util.ArrayList; import java.util.List; public class NetConnectUtil { @@ -117,48 +118,51 @@ public Object sendAndWait(final IdentifiableNetEvent event) { return new ChatMessage(null, Localizer.getInstance().getMessage("lblHostingPortOnN", String.valueOf(port))); } - public static void copyHostedServerUrl() { - final Localizer localizer = Localizer.getInstance(); - String internalAddress = FServerManager.getLocalAddress(); - String externalAddress = FServerManager.getExternalAddress(); - String internalUrl = internalAddress + ":" + FModel.getNetPreferences().getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT); - String externalUrl = null; - if (externalAddress != null) { - externalUrl = externalAddress + ":" + FModel.getNetPreferences().getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT); - GuiBase.getInterface().copyToClipboard(externalUrl); - } else { - GuiBase.getInterface().copyToClipboard(internalUrl); + /** + * Snapshot of the hosted server's reachable addresses, used by the desktop and mobile + * server-URL dialogs. {@code starIndex} is the row to auto-copy and visually mark — either + * the previously remembered URL (if still present) or the first row as a fallback. + */ + public static final class ServerAddressList { + public final List labels; + public final List urls; + public final int starIndex; + + ServerAddressList(final List labels, final List urls, final int starIndex) { + this.labels = labels; + this.urls = urls; + this.starIndex = starIndex; } + } - String message; - String title = localizer.getMessage("lblServerURL"); - List options; - int closeIndex; - int localCopyIndex; - - if (externalUrl != null) { - message = localizer.getMessage("lblShareURLToMakePlayerJoinServer", externalUrl, internalUrl); - options = List.of( - localizer.getMessage("lblCopyExternalURL"), - localizer.getMessage("lblCopyLocalURL"), - localizer.getMessage("lblClose")); - closeIndex = 2; - localCopyIndex = 1; - } else { - message = localizer.getMessage("lblForgeUnableDetermineYourExternalIP", internalUrl); - options = List.of( - localizer.getMessage("lblCopyLocalURL"), - localizer.getMessage("lblClose")); - closeIndex = 1; - localCopyIndex = 0; + public static ServerAddressList collectHostedServerAddresses() { + final ForgeNetPreferences netPrefs = FModel.getNetPreferences(); + final int port = netPrefs.getPrefInt(ForgeNetPreferences.FNetPref.NET_PORT); + final String externalAddress = FServerManager.getExternalAddress(); + + final List labels = new ArrayList<>(); + final List urls = new ArrayList<>(); + if (externalAddress != null) { + labels.add("External (WAN)"); + urls.add(externalAddress + ":" + port); + } + for (final java.util.Map.Entry entry : FServerManager.getAllLocalAddresses().entrySet()) { + labels.add(entry.getKey()); + urls.add(entry.getValue() + ":" + port); } - int result = SOptionPane.showOptionDialog(message, title, SOptionPane.INFORMATION_ICON, options, closeIndex); - if (externalUrl != null && result == 0) { - GuiBase.getInterface().copyToClipboard(externalUrl); - } else if (result == localCopyIndex) { - GuiBase.getInterface().copyToClipboard(internalUrl); + final String rememberedUrl = netPrefs.getPref(ForgeNetPreferences.FNetPref.NET_LAST_COPIED_URL); + int starIndex = urls.indexOf(rememberedUrl); + if (starIndex < 0) { + starIndex = urls.isEmpty() ? -1 : 0; } + return new ServerAddressList(labels, urls, starIndex); + } + + public static void rememberCopiedServerUrl(final String url) { + final ForgeNetPreferences netPrefs = FModel.getNetPreferences(); + netPrefs.setPref(ForgeNetPreferences.FNetPref.NET_LAST_COPIED_URL, url); + netPrefs.save(); } public static ChatMessage join(final String url, final IOnlineLobby onlineLobby, final IOnlineChatInterface chatInterface) {