Skip to content
Draft

Flatlaf #14291

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ subprojects {
implementation(rootProject.libs.java.websocket)
implementation(rootProject.libs.jetbrains.annotations)
implementation(rootProject.libs.xchart)
implementation(rootProject.libs.radiance.substance)
implementation(rootProject.libs.flatlaf)
implementation(rootProject.libs.snakeyaml.engine)
testImplementation(rootProject.libs.jackson.datatype.jsr310)
testImplementation(rootProject.libs.hamcrest.optional)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public void setChatClient(final ChatClient chatClient) {
playerToLobbyConnection.addMessageListener(
PlayerSlapReceivedMessage.TYPE, message -> handleSlapMessage(chatClient, message));

playerToLobbyConnection.addConnectionResetListener(
playerToLobbyConnection::sendConnectToChatMessage);

playerToLobbyConnection.sendConnectToChatMessage();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import lombok.Getter;
import lombok.experimental.UtilityClass;
Expand Down Expand Up @@ -114,7 +113,6 @@ public static <T, E extends Exception> T runInBackgroundAndReturn(
final Consumer<T> runOnEdtBeforeDialogClose,
final Class<E> exceptionType)
throws E, InterruptedException {
checkState(SwingUtilities.isEventDispatchThread());
checkNotNull(message);
checkNotNull(backgroundAction);
checkNotNull(exceptionType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import games.strategy.engine.lobby.client.login.LoginMode;
import games.strategy.engine.lobby.client.login.LoginResult;
import games.strategy.engine.lobby.client.ui.LobbyFrame;
import games.strategy.engine.lobby.client.ui.LobbyModel;
import java.awt.Dimension;
import java.util.Optional;
import java.util.function.Consumer;
Expand All @@ -30,6 +31,7 @@
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.triplea.game.client.HeadedGameRunner;
import org.triplea.swing.SwingComponents;

/** This class provides a way to switch between different ISetupPanel displays. */
@RequiredArgsConstructor
Expand Down Expand Up @@ -132,7 +134,11 @@ public void login() {
}

private void showLobbyWindow(final LoginResult loginResult) {
final LobbyFrame lobbyFrame = new LobbyFrame(loginResult);
final var lobbyModel =
new LobbyModel(
loginResult,
error -> SwingComponents.showError(null, "Error communicating with lobby", error));
final LobbyFrame lobbyFrame = new LobbyFrame(lobbyModel);
MainFrame.hide();
lobbyFrame.setVisible(true);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,86 +1,72 @@
package games.strategy.engine.lobby.client.ui;

import games.strategy.engine.chat.Chat;
import games.strategy.engine.chat.ChatMessagePanel;
import games.strategy.engine.chat.ChatMessagePanel.ChatSoundProfile;
import games.strategy.engine.chat.ChatPlayerPanel;
import games.strategy.engine.chat.ChatTransmitter;
import games.strategy.engine.chat.LobbyChatTransmitter;
import games.strategy.engine.lobby.client.login.LoginResult;
import games.strategy.engine.lobby.client.ui.action.BanPlayerModeratorAction;
import games.strategy.engine.lobby.client.ui.action.DisconnectPlayerModeratorAction;
import games.strategy.engine.lobby.client.ui.action.MutePlayerAction;
import games.strategy.engine.lobby.client.ui.action.player.info.ShowPlayerInformationAction;
import games.strategy.triplea.EngineImageLoader;
import games.strategy.triplea.settings.ClientSetting;
import games.strategy.triplea.ui.QuitHandler;
import games.strategy.triplea.ui.menubar.LobbyMenu;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
import java.util.Optional;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
import lombok.Getter;
import org.triplea.domain.data.ChatParticipant;
import org.triplea.game.client.HeadedGameRunner;
import org.triplea.http.client.web.socket.client.connections.PlayerToLobbyConnection;
import org.triplea.http.client.web.socket.WebSocket;
import org.triplea.sound.ClipPlayer;
import org.triplea.swing.DialogBuilder;
import org.triplea.swing.SwingComponents;

/** The top-level frame window for the lobby client UI. */
public class LobbyFrame extends JFrame implements QuitHandler {
private static final long serialVersionUID = -388371674076362572L;

@Getter private final LoginResult loginResult;
private final ChatTransmitter chatTransmitter;
private final LobbyGameTableModel tableModel;
@Getter private final LobbyModel lobbyModel;

public LobbyFrame(final LoginResult loginResult) {
public LobbyFrame(final LobbyModel lobbyModel) {
super("TripleA Lobby");
this.lobbyModel = lobbyModel;
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
SwingComponents.addWindowClosedListener(this, HeadedGameRunner::exitGameIfNoWindowsVisible);
setIconImage(EngineImageLoader.loadFrameIcon());
this.loginResult = loginResult;

PlayerToLobbyConnection playerToLobbyConnection =
new PlayerToLobbyConnection(
ClientSetting.lobbyUri.getValueOrThrow(),
loginResult.getApiKey(),
error -> SwingComponents.showError(null, "Error communicating with lobby", error));
final var loginResult = lobbyModel.getLoginResult();
final var connection = lobbyModel.getConnection();

setJMenuBar(new LobbyMenu(this, loginResult, playerToLobbyConnection));
playerToLobbyConnection.addConnectionTerminatedListener(
reason -> {
DialogBuilder.builder()
.parent(this)
.title("Connection to Lobby Closed")
.errorMessage("Connection closed: " + reason)
.showDialog();
shutdown();
setJMenuBar(new LobbyMenu(this, loginResult, connection));
connection.addConnectionClosedListener(this::shutdown);
final var gameListingModel = lobbyModel.getGameListingModel();
final var reconnectOverlay = new ReconnectOverlay(this, this::shutdown);
connection.addReconnectionListener(
new WebSocket.ReconnectionHandler() {
@Override
public void onReconnecting(final int attempt) {
reconnectOverlay.show(attempt);
}

@Override
public void onReconnected() {
gameListingModel.refresh();
reconnectOverlay.dismiss();
}
});
playerToLobbyConnection.addConnectionClosedListener(this::shutdown);

chatTransmitter = new LobbyChatTransmitter(playerToLobbyConnection, loginResult.getUsername());
final Chat chat = new Chat(chatTransmitter);
final ChatMessagePanel chatMessagePanel =
new ChatMessagePanel(chat, ChatSoundProfile.LOBBY, new ClipPlayer());
new ChatMessagePanel(lobbyModel.getChat(), ChatSoundProfile.LOBBY, new ClipPlayer());
Optional.ofNullable(loginResult.getLoginMessage())
.ifPresent(chatMessagePanel::addServerMessage);
final ChatPlayerPanel chatPlayers = new ChatPlayerPanel(chat);

final ChatPlayerPanel chatPlayers = new ChatPlayerPanel(lobbyModel.getChat());
chatPlayers.setPreferredSize(new Dimension(200, 600));
chatPlayers.addActionFactory(
clickedChatter ->
lobbyPlayerRightClickMenuActions(
this, loginResult, clickedChatter, playerToLobbyConnection));
LobbyPlayerActions.buildFor(this, loginResult, clickedChatter, connection));

tableModel = new LobbyGameTableModel(loginResult.isModerator(), playerToLobbyConnection);
final var tableModel = new LobbyGameTableModel(loginResult.isModerator(), gameListingModel);
final LobbyGamePanel gamePanel =
new LobbyGamePanel(this, loginResult, tableModel, playerToLobbyConnection);
new LobbyGamePanel(this, loginResult, tableModel, gameListingModel);

final JSplitPane leftSplit = new JSplitPane();
leftSplit.setOrientation(JSplitPane.VERTICAL_SPLIT);
Expand All @@ -107,58 +93,11 @@ public void windowClosing(final WindowEvent e) {
});
}

private static List<Action> lobbyPlayerRightClickMenuActions(
JFrame parentWindow,
LoginResult loginResult,
ChatParticipant clickedOn,
PlayerToLobbyConnection playerToLobbyConnection) {
if (clickedOn.getUserName().equals(loginResult.getUsername())) {
return List.of();
}

final var showPlayerInformationAction =
ShowPlayerInformationAction.builder()
.parent(parentWindow)
.playerChatId(clickedOn.getPlayerChatId())
.playerName(clickedOn.getUserName())
.playerToLobbyConnection(playerToLobbyConnection)
.build()
.toSwingAction();

if (!loginResult.isModerator()) {
return List.of(showPlayerInformationAction);
}
return List.of(
showPlayerInformationAction,
MutePlayerAction.builder()
.parent(parentWindow)
.playerChatId(clickedOn.getPlayerChatId())
.playerToLobbyConnection(playerToLobbyConnection)
.playerName(clickedOn.getUserName().getValue())
.build()
.toSwingAction(),
DisconnectPlayerModeratorAction.builder()
.parent(parentWindow)
.playerToLobbyConnection(playerToLobbyConnection)
.playerChatId(clickedOn.getPlayerChatId())
.userName(clickedOn.getUserName())
.build()
.toSwingAction(),
BanPlayerModeratorAction.builder()
.parent(parentWindow)
.playerToLobbyConnection(playerToLobbyConnection)
.playerChatIdToBan(clickedOn.getPlayerChatId())
.playerName(clickedOn.getUserName().getValue())
.build()
.toSwingAction());
}

@Override
public boolean shutdown() {
setVisible(false);
dispose();
chatTransmitter.disconnect();
tableModel.shutdown();
lobbyModel.shutdown();
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package games.strategy.engine.lobby.client.ui;

import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.SwingUtilities;
import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing;
import org.triplea.http.client.web.socket.client.connections.PlayerToLobbyConnection;
import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage;
import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameUpdatedMessage;
import org.triplea.lobby.common.GameDescription;
import org.triplea.lobby.common.LobbyGameUpdateListener;

/**
* Pure-Java model for the lobby game list. Owns the connection message listeners, the live game
* list, and all network operations (boot / shutdown-signal). Has no Swing dependency.
*/
class LobbyGameListingModel {
private final PlayerToLobbyConnection connection;
private final List<LobbyGameListing> games = new CopyOnWriteArrayList<>();
private final List<Runnable> changeListeners = new ArrayList<>();

private final LobbyGameUpdateListener lobbyGameBroadcaster =
new LobbyGameUpdateListener() {
@Override
public void gameUpdated(final LobbyGameListing lobbyGameListing) {
updateGame(lobbyGameListing);
}

@Override
public void gameRemoved(final String gameId) {
removeGame(gameId);
}
};

LobbyGameListingModel(final PlayerToLobbyConnection connection) {
this.connection = connection;

connection.addMessageListener(
LobbyGameUpdatedMessage.TYPE,
msg -> lobbyGameBroadcaster.gameUpdated(msg.getLobbyGameListing()));

connection.addMessageListener(
LobbyGameRemovedMessage.TYPE, msg -> lobbyGameBroadcaster.gameRemoved(msg.getGameId()));

connection.fetchGameListing().forEach(lobbyGameBroadcaster::gameUpdated);
}

@VisibleForTesting
LobbyGameUpdateListener getLobbyGameBroadcaster() {
return lobbyGameBroadcaster;
}

int getRowCount() {
return games.size();
}

GameDescription getGameDescriptionForRow(final int i) {
return GameDescription.fromLobbyGame(games.get(i).getLobbyGame());
}

LobbyGameListing getGameListingForRow(final int i) {
return games.get(i);
}

String getGameIdForRow(final int i) {
return games.get(i).getGameId();
}

void bootGame(final String gameId) {
connection.bootGame(gameId);
}

void sendShutdownRequest(final String gameId) {
connection.sendShutdownRequest(gameId);
}

PlayerToLobbyConnection getConnection() {
return connection;
}

void addChangeListener(final Runnable listener) {
changeListeners.add(listener);
}

/**
* Clears the current game list and re-fetches it from the server. Called after a successful
* reconnect to remove stale entries.
*
* <p>The network fetch runs on the calling thread; the list mutation and table notification are
* then posted to the EDT so that Swing never observes a cleared list with a stale row count.
*/
void refresh() {
final List<LobbyGameListing> freshListing = connection.fetchGameListing();
SwingUtilities.invokeLater(
() -> {
games.clear();
freshListing.forEach(this::updateGame);
// Notify in case freshListing was empty (updateGame won't fire in that case).
notifyListeners();
});
}

private void notifyListeners() {
changeListeners.forEach(Runnable::run);
}

private void updateGame(final LobbyGameListing lobbyGameListing) {
final LobbyGameListing toReplace = findGame(lobbyGameListing.getGameId());
if (toReplace == null) {
games.add(lobbyGameListing);
} else {
final int replaceIndex = games.indexOf(toReplace);
games.set(replaceIndex, lobbyGameListing);
}
notifyListeners();
}

private void removeGame(final String gameId) {
if (gameId == null) {
return;
}
final LobbyGameListing gameToRemove = findGame(gameId);
if (gameToRemove != null) {
games.remove(gameToRemove);
notifyListeners();
}
}

private LobbyGameListing findGame(final String gameId) {
return games.stream().filter(game -> game.getGameId().equals(gameId)).findFirst().orElse(null);
}
}
Loading
Loading