Skip to content

Commit

Permalink
Allow private chat tab to be garbage collected when closed
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheikah45 committed Apr 20, 2023
1 parent 8eb4a9e commit 78cceec
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import javafx.scene.text.TextFlow;
import javafx.scene.web.WebView;
Expand All @@ -39,7 +40,7 @@ public class MatchmakingChatController extends AbstractChatTabController {

public Tab matchmakingChatTabRoot;
public WebView messagesWebView;
public TextInputControl messageTextField;
public TextField messageTextField;
public TextFlow topicText;
public Hyperlink discordLink;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
import com.faforever.client.util.TimeService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.eventbus.EventBus;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.collections.MapChangeListener.Change;
import javafx.collections.WeakMapChangeListener;
import javafx.event.Event;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputControl;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
Expand All @@ -50,7 +52,7 @@ public class PrivateChatTabController extends AbstractChatTabController {
public ImageView avatarImageView;
public Region defaultIconImageView;
public WebView messagesWebView;
public TextInputControl messageTextField;
public TextField messageTextField;
public PrivatePlayerInfoController privatePlayerInfoController;
public ScrollPane gameDetailScrollPane;

Expand All @@ -71,6 +73,13 @@ public PrivateChatTabController(UserService userService, PreferencesService pref
this.avatarService = avatarService;
}

public void initialize() {
super.initialize();
JavaFxUtil.bindManagedToVisible(avatarImageView, defaultIconImageView);
avatarImageView.visibleProperty().bind(avatarImageView.imageProperty().isNotNull());
defaultIconImageView.visibleProperty().bind(avatarImageView.imageProperty().isNull());
userOffline = false;
}

boolean isUserOffline() {
return userOffline;
Expand All @@ -84,30 +93,25 @@ public Tab getRoot() {
@Override
public void setReceiver(String username) {
super.setReceiver(username);

ObservableValue<Boolean> showing = getRoot().tabPaneProperty().flatMap(JavaFxUtil::showingProperty);

privateChatTabRoot.setId(username);
privateChatTabRoot.setText(username);
playerService.getPlayerByNameIfOnline(username)
.ifPresent(player -> avatarImageView.imageProperty()
.bind(player.avatarProperty().map(avatarService::loadAvatar)));
.bind(player.avatarProperty().map(avatarService::loadAvatar).when(showing)));
ChatChannelUser chatUser = chatService.getOrCreateChatUser(username, username);
privatePlayerInfoController.setChatUser(chatUser);
chatService.addUsersListener(username, new WeakMapChangeListener<>(chatUsersByNameListener));
}

@Override
protected void onClosed(Event event) {
super.onClosed(event);
privatePlayerInfoController.dispose();
}

public void initialize() {
super.initialize();
JavaFxUtil.bindManagedToVisible(avatarImageView, defaultIconImageView);
avatarImageView.visibleProperty().bind(avatarImageView.imageProperty().isNotNull());
defaultIconImageView.visibleProperty().bind(avatarImageView.imageProperty().isNull());
JavaFxUtil.fixScrollSpeed(gameDetailScrollPane);
userOffline = false;
}

@Override
protected TextInputControl messageTextField() {
return messageTextField;
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/com/faforever/client/domain/GameBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyMapProperty;
import javafx.beans.property.ReadOnlyMapWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
Expand All @@ -18,7 +18,6 @@
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableIntegerValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
Expand All @@ -32,6 +31,7 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -65,8 +65,8 @@ public class GameBean {
/**
* Maps a sim mod's UID to its name.
*/
ReadOnlyMapWrapper<String, String> simMods = new ReadOnlyMapWrapper<>(FXCollections.emptyObservableMap());
ReadOnlyMapWrapper<Integer, List<Integer>> teams = new ReadOnlyMapWrapper<>(FXCollections.emptyObservableMap());
ReadOnlyObjectWrapper<Map<String, String>> simMods = new ReadOnlyObjectWrapper<>(Map.of());
ReadOnlyObjectWrapper<Map<Integer, List<Integer>>> teams = new ReadOnlyObjectWrapper<>(Map.of());
ObservableValue<Set<Integer>> allPlayersInGame = teams.map(team -> team.values()
.stream()
.flatMap(Collection::stream)
Expand Down Expand Up @@ -246,14 +246,14 @@ public Map<String, String> getSimMods() {
}

public void setSimMods(Map<String, String> simMods) {
if (this.simMods.equals(simMods)) {
if (Objects.equals(this.simMods.get(), simMods)) {
return;
}

this.simMods.set(simMods == null ? FXCollections.emptyObservableMap() : FXCollections.unmodifiableObservableMap(FXCollections.observableMap(simMods)));
this.simMods.set(simMods == null ? Map.of() : Map.copyOf(simMods));
}

public ReadOnlyMapProperty<String, String> simModsProperty() {
public ReadOnlyObjectProperty<Map<String, String>> simModsProperty() {
return simMods.getReadOnlyProperty();
}

Expand All @@ -265,14 +265,14 @@ public Map<Integer, List<Integer>> getTeams() {
}

public void setTeams(Map<Integer, List<Integer>> teams) {
if (this.teams.equals(teams)) {
if (Objects.equals(this.teams.get(), teams)) {
return;
}

this.teams.set(teams == null ? FXCollections.emptyObservableMap() : FXCollections.unmodifiableObservableMap(FXCollections.observableMap(teams)));
this.teams.set(teams == null ? Map.of() : Map.copyOf(teams));
}

public ReadOnlyMapProperty<Integer, List<Integer>> teamsProperty() {
public ReadOnlyObjectProperty<Map<Integer, List<Integer>>> teamsProperty() {
return teams.getReadOnlyProperty();
}

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/faforever/client/fx/JavaFxUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.MapProperty;
Expand All @@ -27,6 +28,7 @@
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
Expand All @@ -35,6 +37,7 @@
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter;
Expand Down Expand Up @@ -186,6 +189,14 @@ public static String toRgbCode(Color color) {
(int) (color.getBlue() * 255));
}

public static ObservableValue<Boolean> showingProperty(Node node) {
ObservableValue<Boolean> attachedToVisibleWindow = node.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
.orElse(false);
return node.visibleProperty().and(BooleanExpression.booleanExpression(attachedToVisibleWindow));
}

/**
* Returns an unmodifiable observable list from the specified list that mirrors any changes made to the specified
* map.
Expand Down
86 changes: 50 additions & 36 deletions src/main/java/com/faforever/client/game/GameDetailController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.faforever.client.domain.FeaturedModBean;
import com.faforever.client.domain.GameBean;
import com.faforever.client.domain.PlayerBean;
import com.faforever.client.fx.Controller;
import com.faforever.client.fx.ImageViewHelper;
import com.faforever.client.fx.JavaFxService;
Expand Down Expand Up @@ -57,9 +58,10 @@
import reactor.core.publisher.Mono;

import java.time.OffsetDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

@Component
@Slf4j
Expand All @@ -82,8 +84,11 @@ public class GameDetailController implements Controller<Pane> {

private final ObjectProperty<GameBean> game = new SimpleObjectProperty<>();
private final BooleanProperty playtimeVisible = new SimpleBooleanProperty();
private final MapProperty<Integer, List<Integer>> teams = new SimpleMapProperty<>(FXCollections.emptyObservableMap());
private final ObservableList<Integer> teamIds = new SortedList<>(JavaFxUtil.attachListToMapKeys(FXCollections.observableArrayList(), teams), Comparator.naturalOrder());
private final ObservableValue<Map<Integer, List<Integer>>> teams = game.flatMap(GameBean::teamsProperty).orElse(Map.of());
private final ObservableValue<List<Integer>> teamIds = teams.map(teamMap -> teamMap.keySet()
.stream()
.sorted()
.toList());
private final ObservableValue<String> leaderboard = game.flatMap(GameBean::leaderboardProperty);
private final Timeline playTimeTimeline = new Timeline(new KeyFrame(Duration.ZERO, event -> updatePlaytimeValue()), new KeyFrame(Duration.seconds(1)));

Expand All @@ -103,11 +108,15 @@ public class GameDetailController implements Controller<Pane> {
public Button generateMapButton;

public void initialize() {
playTimeTimeline.setCycleCount(Timeline.INDEFINITE);

contextMenuBuilder.addCopyLabelContextMenu(gameTitleLabel, mapLabel, gameTypeLabel);
JavaFxUtil.bindManagedToVisible(root, joinButton, watchButton, gameTitleLabel, hostLabel, mapLabel, numberOfPlayersLabel, mapPreviewContainer, gameTypeLabel, playtimeLabel, generateMapButton);
JavaFxUtil.bind(mapPreviewContainer.visibleProperty(), mapImageView.imageProperty().isNotNull());

contextMenuBuilder.addCopyLabelContextMenu(gameTitleLabel, mapLabel, gameTypeLabel);

ObservableValue<Boolean> showing = JavaFxUtil.showingProperty(getRoot());

playTimeTimeline.setCycleCount(Timeline.INDEFINITE);

root.parentProperty().addListener(observable -> {
if (!(root.getParent() instanceof Pane)) {
return;
Expand All @@ -116,57 +125,64 @@ public void initialize() {
});

root.visibleProperty().bind(game.isNotNull());
gameTitleLabel.textProperty().bind(game.flatMap(GameBean::titleProperty).map(StringUtils::normalizeSpace));
hostLabel.textProperty().bind(game.flatMap(GameBean::hostProperty));
mapLabel.textProperty().bind(game.flatMap(GameBean::mapFolderNameProperty));
gameTitleLabel.textProperty()
.bind(game.flatMap(GameBean::titleProperty).map(StringUtils::normalizeSpace).when(showing));
hostLabel.textProperty().bind(game.flatMap(GameBean::hostProperty).when(showing));

ObservableValue<String> mapFolderNameObservable = game.flatMap(GameBean::mapFolderNameProperty);
mapLabel.textProperty().bind(mapFolderNameObservable.when(showing));
mapImageView.imageProperty()
.bind(game.flatMap(GameBean::mapFolderNameProperty)
.map(folderName -> mapService.loadPreview(folderName, PreviewSize.LARGE))
.flatMap(imageViewHelper::createPlaceholderImageOnErrorObservable));
.bind(mapFolderNameObservable.map(folderName -> mapService.loadPreview(folderName, PreviewSize.LARGE))
.flatMap(imageViewHelper::createPlaceholderImageOnErrorObservable)
.when(showing));

game.flatMap(GameBean::featuredModProperty).addListener((SimpleChangeListener<String>) this::onFeaturedModChanged);
game.flatMap(GameBean::featuredModProperty)
.when(showing)
.addListener((SimpleChangeListener<String>) this::onFeaturedModChanged);

watchButtonController.gameProperty().bind(game);

watchButton.visibleProperty()
.bind(game.flatMap(gameBean -> gameBean.statusProperty()
.isEqualTo(GameStatus.PLAYING)
.and(gameBean.startTimeProperty().isNotNull())).orElse(false));
.and(gameBean.startTimeProperty().isNotNull())).orElse(false).when(showing));
joinButton.visibleProperty()
.bind(game.flatMap(gameBean -> gameBean.statusProperty()
.isEqualTo(GameStatus.OPEN)
.and(gameBean.gameTypeProperty().isNotEqualTo(GameType.MATCHMAKER))).orElse(false));
.and(gameBean.gameTypeProperty().isNotEqualTo(GameType.MATCHMAKER))).orElse(false).when(showing));

generateMapButton.visibleProperty()
.bind(game.flatMap(GameBean::mapFolderNameProperty)
.map(mapName -> mapGeneratorService.isGeneratedMap(mapName) && !mapService.isInstalled(mapName))
.orElse(false));
.bind(mapFolderNameObservable.map(mapName -> mapGeneratorService.isGeneratedMap(mapName) && !mapService.isInstalled(mapName))
.orElse(false)
.when(showing));

playtimeLabel.visibleProperty()
.bind(playtimeVisible.and(BooleanExpression.booleanExpression(game.flatMap(gameBean -> gameBean.startTimeProperty()
.isNotNull().and(gameBean.statusProperty().isEqualTo(GameStatus.PLAYING))))).and(playTimeTimeline.statusProperty().isEqualTo(Status.RUNNING)));
.isNotNull()
.and(gameBean.statusProperty().isEqualTo(GameStatus.PLAYING)))))
.and(playTimeTimeline.statusProperty().isEqualTo(Status.RUNNING))
.when(showing));

numberOfPlayersLabel.textProperty()
.bind(game.flatMap(gameBean -> gameBean.numActivePlayersProperty()
.flatMap(numActive -> gameBean.maxPlayersProperty()
.map(numMax -> i18n.get("game.detail.players.format", numActive, numMax)))));
.bind(game.flatMap(gameBean -> Bindings.createStringBinding(() -> i18n.get("game.detail.players.format", gameBean.getNumActivePlayers(), gameBean.getMaxPlayers()), gameBean.numActivePlayersProperty(), gameBean.maxPlayersProperty()))
.when(showing));

game.flatMap(GameBean::statusProperty).addListener((SimpleChangeListener<GameStatus>) this::onGameStatusChanged);

teams.bind(game.flatMap(GameBean::teamsProperty));
game.flatMap(GameBean::statusProperty).when(showing)
.addListener((SimpleChangeListener<GameStatus>) this::onGameStatusChanged);

for (int i = -1; i < 8; i++) {
TeamCardController teamCardController = uiService.loadFxml("theme/team_card.fxml");
teamCardController.bindPlayersToPlayerIds();
teamCardController.setRatingPrecision(RatingPrecision.ROUNDED);
teamCardController.ratingProviderProperty()
.bind(leaderboard.map(name -> player -> RatingUtil.getLeaderboardRating(player, name)));
.bind(leaderboard.map(name -> (Function<PlayerBean, Integer>) player -> RatingUtil.getLeaderboardRating(player, name))
.when(showing));
teamCardController.playerIdsProperty()
.bind(Bindings.valueAt(teams, teamCardController.teamIdProperty().asObject())
.map(FXCollections::observableList));
teamCardController.teamIdProperty().bind(Bindings.valueAt(teamIds, i + 1));
.bind(Bindings.createObjectBinding(() -> teams.getValue().get(teamCardController.getTeamId()), teams.when(showing), teamCardController.teamIdProperty()).when(showing));
int index = i + 1;
teamCardController.teamIdProperty().bind(teamIds.map(ids -> index < ids.size() ? ids.get(index) : null).when(showing));
Node teamCardControllerRoot = teamCardController.getRoot();
teamCardControllerRoot.visibleProperty().bind(teamCardController.playerIdsProperty().emptyProperty().not());
teamCardControllerRoot.visibleProperty().bind(teamCardController.playerIdsProperty().map(players -> !players.isEmpty()));
JavaFxUtil.bindManagedToVisible(teamCardControllerRoot);
teamListPane.getChildren().add(teamCardControllerRoot);
}
Expand Down Expand Up @@ -195,17 +211,13 @@ private void startPlayTime() {
}
}

private void stopPlaytime() {
JavaFxUtil.runLater(playTimeTimeline::stop);
}

private void updatePlaytimeValue() {
String durationText;

GameBean gameBean = getGame();
if (gameBean == null || gameBean.getStartTime() == null || gameBean.getStatus() != GameStatus.PLAYING) {
durationText = null;
stopPlaytime();
playTimeTimeline.stop();
} else {
durationText = timeService.shortDuration(java.time.Duration.between(gameBean.getStartTime(), OffsetDateTime.now()));
}
Expand All @@ -214,7 +226,9 @@ private void updatePlaytimeValue() {
}

public void dispose() {
stopPlaytime();
playTimeTimeline.stop();
watchButtonController.dispose();
eventBus.unregister(this);
}

public void setGame(GameBean game) {
Expand Down
Loading

0 comments on commit 78cceec

Please sign in to comment.