From ac45f8d1f3caffc0edd602a8435e19afca529b6d Mon Sep 17 00:00:00 2001 From: Kirill Riman <55841348+K-ETFreeman@users.noreply.github.com> Date: Mon, 22 Apr 2024 03:19:11 +0300 Subject: [PATCH] Feature/#2319 veto system: simple maplist popup (#3151) --- .../faforever/client/api/FafApiAccessor.java | 2 +- .../domain/api/MatchmakerQueueMapPool.java | 4 +- .../com/faforever/client/map/MapService.java | 53 ++-- .../client/map/MapVaultController.java | 10 +- .../client/mapstruct/MatchmakerMapper.java | 4 +- .../MatchmakingQueueItemController.java | 7 +- .../TeamMatchmakingController.java | 14 + .../TeamMatchmakingMapListController.java | 258 ++++++++++++++++++ .../TeamMatchmakingMapTileController.java | 101 +++++++ .../com/faforever/client/theme/UiService.java | 10 +- .../client/ui/dialog/DialogLayout.java | 18 +- .../client/vault/VaultEntityController.java | 2 +- .../resources/css/controls/dialog-layout.css | 4 + .../teammatchmaking/matchmaking_map_tile.fxml | 31 +++ .../matchmaking_maplist_popup.fxml | 24 ++ src/main/resources/theme/style.css | 14 + .../faforever/client/map/MapServiceTest.java | 50 +--- .../MatchmakingQueueItemControllerTest.java | 7 - 18 files changed, 502 insertions(+), 111 deletions(-) create mode 100644 src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapListController.java create mode 100644 src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapTileController.java create mode 100644 src/main/resources/theme/play/teammatchmaking/matchmaking_map_tile.fxml create mode 100644 src/main/resources/theme/play/teammatchmaking/matchmaking_maplist_popup.fxml diff --git a/src/main/java/com/faforever/client/api/FafApiAccessor.java b/src/main/java/com/faforever/client/api/FafApiAccessor.java index cb9f613e44..fe63fb14a7 100644 --- a/src/main/java/com/faforever/client/api/FafApiAccessor.java +++ b/src/main/java/com/faforever/client/api/FafApiAccessor.java @@ -103,7 +103,7 @@ public class FafApiAccessor implements InitializingBean { java.util.Map.entry(MapVersion.class, List.of("map", "map.reviewsSummary", "map.author")), java.util.Map.entry(MapReviewsSummary.class, List.of("map.latestVersion", "map.author", "map.reviewsSummary")), java.util.Map.entry(Map.class, List.of("latestVersion", "author", "reviewsSummary")), - java.util.Map.entry(MapPoolAssignment.class, List.of("mapVersion", "mapVersion.map", "mapVersion.map.author", "mapVersion.map.reviewsSummary")), + java.util.Map.entry(MapPoolAssignment.class, List.of("mapVersion", "mapVersion.map", "mapVersion.map.author", "mapVersion.map.reviewsSummary", "mapPool", "mapPool.matchmakerQueueMapPool")), java.util.Map.entry(ModVersion.class, List.of("mod", "mod.latestVersion", "mod.reviewsSummary", "mod.uploader")), java.util.Map.entry(ModReviewsSummary.class, List.of("mod.latestVersion", "mod.reviewsSummary", "mod.uploader")), java.util.Map.entry(Mod.class, List.of("latestVersion", "reviewsSummary", "uploader")), diff --git a/src/main/java/com/faforever/client/domain/api/MatchmakerQueueMapPool.java b/src/main/java/com/faforever/client/domain/api/MatchmakerQueueMapPool.java index d68c566770..a180ce255b 100644 --- a/src/main/java/com/faforever/client/domain/api/MatchmakerQueueMapPool.java +++ b/src/main/java/com/faforever/client/domain/api/MatchmakerQueueMapPool.java @@ -4,6 +4,6 @@ public record MatchmakerQueueMapPool( Integer id, - double minRating, - double maxRating, MatchmakerQueueInfo matchmakerQueue, MapPool mapPool + Double minRating, + Double maxRating, MatchmakerQueueInfo matchmakerQueue ) {} diff --git a/src/main/java/com/faforever/client/map/MapService.java b/src/main/java/com/faforever/client/map/MapService.java index f3da9f69e7..16f271fae1 100644 --- a/src/main/java/com/faforever/client/map/MapService.java +++ b/src/main/java/com/faforever/client/map/MapService.java @@ -7,6 +7,7 @@ import com.faforever.client.domain.api.Map; import com.faforever.client.domain.api.MapType; import com.faforever.client.domain.api.MapVersion; +import com.faforever.client.domain.api.MatchmakerQueueMapPool; import com.faforever.client.domain.server.MatchmakerQueueInfo; import com.faforever.client.domain.server.PlayerInfo; import com.faforever.client.exception.AssetLoadException; @@ -16,6 +17,7 @@ import com.faforever.client.i18n.I18n; import com.faforever.client.map.generator.MapGeneratorService; import com.faforever.client.mapstruct.MapMapper; +import com.faforever.client.mapstruct.MatchmakerMapper; import com.faforever.client.notification.NotificationService; import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.ForgedAlliancePrefs; @@ -34,9 +36,6 @@ import com.faforever.commons.api.elide.ElideNavigator; import com.faforever.commons.api.elide.ElideNavigatorOnCollection; import com.faforever.commons.api.elide.ElideNavigatorOnId; -import com.github.rutledgepaulv.qbuilders.builders.QBuilder; -import com.github.rutledgepaulv.qbuilders.conditions.Condition; -import com.github.rutledgepaulv.qbuilders.visitors.RSQLVisitor; import com.google.common.annotations.VisibleForTesting; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; @@ -64,7 +63,6 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.retry.Retry; @@ -80,13 +78,13 @@ import java.nio.file.WatchService; import java.time.Duration; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; import static com.faforever.client.util.LuaUtil.loadFile; @@ -117,6 +115,7 @@ public class MapService implements InitializingBean, DisposableBean { private final MapGeneratorService mapGeneratorService; private final PlayerService playerService; private final MapMapper mapMapper; + private final MatchmakerMapper matchmakerMapper; private final FileSizeReader fileSizeReader; private final ClientProperties clientProperties; private final ForgedAlliancePrefs forgedAlliancePrefs; @@ -626,38 +625,18 @@ public Mono downloadAllMatchmakerMaps(MatchmakerQueueInfo matchmakerQueue) } @Cacheable(value = CacheNames.MATCHMAKER_POOLS, sync = true) - @SuppressWarnings({"rawtypes", "unchecked"}) - public Mono, Integer>> getMatchmakerMapsWithPageCount(MatchmakerQueueInfo matchmakerQueue, - int count, int page) { - PlayerInfo player = playerService.getCurrentPlayer(); - double rating = Optional.ofNullable(player.getLeaderboardRatings()) - .map(ratings -> ratings.get(matchmakerQueue.getLeaderboard().technicalName())) - .map(ratingBean -> ratingBean.mean() - 3 * ratingBean.deviation()) - .orElse(0d); - ElideNavigatorOnCollection navigator = ElideNavigator.of(MapPoolAssignment.class).collection(); - List> conditions = new ArrayList<>(); - conditions.add(qBuilder().intNum("mapPool.matchmakerQueueMapPool.matchmakerQueue.id").eq(matchmakerQueue.getId())); - conditions.add(qBuilder().doubleNum("mapPool.matchmakerQueueMapPool.minRating") - .lte(rating) - .or() - .floatNum("mapPool.matchmakerQueueMapPool.minRating") - .ne(null)); - // The api doesn't support the ne operation so we manually replace it with isnull which rsql does not support - String customFilter = ((String) new QBuilder().and(conditions).query(new RSQLVisitor())).replace("ex", "isnull"); - Flux matchmakerMapsFlux = fafApiAccessor.getMany(navigator, customFilter) - .map(mapMapper::mapFromPoolAssignment) - .distinct() - .sort( - Comparator.nullsLast(Comparator.comparing(MapVersion::size)) - .thenComparing(Comparator.nullsLast( - Comparator.comparing(MapVersion::map, - Comparator.nullsLast( - Comparator.comparing( - Map::displayName, - Comparator.nullsLast( - String.CASE_INSENSITIVE_ORDER))))))); - return Mono.zip(matchmakerMapsFlux.skip((long) (page - 1) * count).take(count).collectList(), - matchmakerMapsFlux.count().map(size -> (int) (size - 1) / count + 1)); + public Mono >> getMatchmakerBrackets(MatchmakerQueueInfo matchmakerQueue) { + ElideNavigatorOnCollection navigator = ElideNavigator + .of(MapPoolAssignment.class).collection() + .setFilter(qBuilder().intNum("mapPool.matchmakerQueueMapPool.matchmakerQueue.id").eq(matchmakerQueue.getId())); + + return fafApiAccessor.getMany(navigator) + .map(matchmakerMapper::map) + .collect(Collectors.groupingBy(assignment -> assignment.mapPool().mapPool(), + Collectors.mapping( + com.faforever.client.domain.api.MapPoolAssignment::mapVersion, + Collectors.toList()))); + } public Mono hasPlayedMap(PlayerInfo player, MapVersion mapVersion) { diff --git a/src/main/java/com/faforever/client/map/MapVaultController.java b/src/main/java/com/faforever/client/map/MapVaultController.java index 8229c4e6a0..1d7e9dbe2d 100644 --- a/src/main/java/com/faforever/client/map/MapVaultController.java +++ b/src/main/java/com/faforever/client/map/MapVaultController.java @@ -112,8 +112,6 @@ protected void setSupplier(SearchConfig searchConfig) { case NEWEST -> mapService.getNewestMapsWithPageCount(pageSize, pagination.getCurrentPageIndex() + 1); case HIGHEST_RATED -> mapService.getHighestRatedMapsWithPageCount(pageSize, pagination.getCurrentPageIndex() + 1); case PLAYED -> mapService.getMostPlayedMapsWithPageCount(pageSize, pagination.getCurrentPageIndex() + 1); - case MAP_POOL -> - mapService.getMatchmakerMapsWithPageCount(matchmakerQueue, pageSize, pagination.getCurrentPageIndex() + 1); case OWN -> mapService.getOwnedMapsWithPageCount(pageSize, pagination.getCurrentPageIndex() + 1); case PLAYER, HIGHEST_RATED_UI -> throw new UnsupportedOperationException(); }; @@ -174,13 +172,7 @@ protected Class getDefaultNavigateEvent() { @Override protected void handleSpecialNavigateEvent(NavigateEvent navigateEvent) { - if (navigateEvent instanceof ShowMapPoolEvent showMapPoolEvent) { - matchmakerQueue = showMapPoolEvent.getQueue(); - searchType = SearchType.MAP_POOL; - onPageChange(null, true); - } else { - log.warn("No such NavigateEvent for this Controller: {}", navigateEvent.getClass()); - } + log.warn("No such NavigateEvent for this Controller: {}", navigateEvent.getClass()); } private void openUploadWindow(Path path) { diff --git a/src/main/java/com/faforever/client/mapstruct/MatchmakerMapper.java b/src/main/java/com/faforever/client/mapstruct/MatchmakerMapper.java index 7a22814d5a..d06225e483 100644 --- a/src/main/java/com/faforever/client/mapstruct/MatchmakerMapper.java +++ b/src/main/java/com/faforever/client/mapstruct/MatchmakerMapper.java @@ -24,7 +24,7 @@ public interface MatchmakerMapper { @Mapping(target = "queuePopTime", source = "popTime") @Mapping(target = "technicalName", source = "name") MatchmakerQueueInfo update(MatchmakerInfo.MatchmakerQueue dto, @MappingTarget MatchmakerQueueInfo bean); - + @Mapping(target = "mapVersion", source = "dto") MapPoolAssignment map(com.faforever.commons.api.dto.MapPoolAssignment dto); @InheritInverseConfiguration @@ -33,7 +33,7 @@ public interface MatchmakerMapper { List mapAssignmentDtos(List dto); List mapAssignmentBeans(List bean); - + @Mapping(target = "mapPool", source = "matchmakerQueueMapPool") MapPool map(com.faforever.commons.api.dto.MapPool dto); @InheritInverseConfiguration diff --git a/src/main/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemController.java b/src/main/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemController.java index 41e9871b01..d000261791 100644 --- a/src/main/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemController.java +++ b/src/main/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemController.java @@ -31,6 +31,7 @@ import java.time.Duration; import java.time.OffsetDateTime; +import java.util.function.Consumer; @Slf4j @Component @@ -60,6 +61,7 @@ public class MatchmakingQueueItemController extends NodeController { private final ObjectProperty queue = new SimpleObjectProperty<>(); private final ObservableValue popTime = queue.flatMap(MatchmakerQueueInfo::queuePopTimeProperty); private Timeline queuePopTimeUpdater; + private Consumer onMapPoolClickedListener; @Override protected void onInitialize() { @@ -170,8 +172,11 @@ private void setQueuePopTimeUpdater() { queuePopTimeUpdater.play(); } + public void setOnMapPoolClickedListener(Consumer onMapPoolClickedListener) { + this.onMapPoolClickedListener = onMapPoolClickedListener; + } public void showMapPool() { - navigationHandler.navigateTo(new ShowMapPoolEvent(getQueue())); + onMapPoolClickedListener.accept(this.queue.get()); } public MatchmakerQueueInfo getQueue() { diff --git a/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingController.java b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingController.java index 5b668e8d08..8d11a4a698 100644 --- a/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingController.java +++ b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingController.java @@ -15,6 +15,9 @@ import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.MatchmakerPrefs; import com.faforever.client.theme.UiService; +import com.faforever.client.ui.dialog.Dialog; +import com.faforever.client.ui.dialog.Dialog.DialogTransition; +import com.faforever.client.ui.dialog.DialogLayout; import com.faforever.commons.lobby.Faction; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; @@ -31,6 +34,7 @@ import javafx.scene.control.TabPane; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; @@ -331,6 +335,15 @@ private void selectFactions(List factions) { factionsToButtons.forEach((faction, toggleButton) -> toggleButton.setSelected(factions.contains(faction))); } + protected void onMapPoolClickedListener(MatchmakerQueueInfo queue) { + TeamMatchmakingMapListController controller = uiService.loadFxml("theme/play/teammatchmaking/matchmaking_maplist_popup.fxml"); + controller.maxWidthProperty().bind(teamMatchmakingRoot.widthProperty()); + controller.maxHeightProperty().bind(teamMatchmakingRoot.heightProperty()); + controller.setQueue(queue); + Pane root = controller.getRoot(); + uiService.showInDialog(teamMatchmakingRoot, root, null, true, DialogTransition.CENTER); + } + private void renderQueues() { List queues = new ArrayList<>(teamMatchmakingService.getQueues()); queues.sort(Comparator.comparing(MatchmakerQueueInfo::getTeamSize).thenComparing(MatchmakerQueueInfo::getId)); @@ -343,6 +356,7 @@ private void renderQueues() { MatchmakingQueueItemController controller = uiService.loadFxml( "theme/play/teammatchmaking/matchmaking_queue_card.fxml"); controller.setQueue(queue); + controller.setOnMapPoolClickedListener(this::onMapPoolClickedListener); controller.getRoot().prefWidthProperty().bind(prefWidth); return controller.getRoot(); }).collect(Collectors.toList()); diff --git a/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapListController.java b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapListController.java new file mode 100644 index 0000000000..421d41c696 --- /dev/null +++ b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapListController.java @@ -0,0 +1,258 @@ +package com.faforever.client.teammatchmaking; + +import com.faforever.client.domain.api.MapVersion; +import com.faforever.client.domain.api.MatchmakerQueueMapPool; +import com.faforever.client.domain.server.MatchmakerQueueInfo; +import com.faforever.client.fx.FxApplicationThreadExecutor; +import com.faforever.client.fx.JavaFxUtil; +import com.faforever.client.fx.NodeController; +import com.faforever.client.map.MapService; +import com.faforever.client.player.PlayerService; +import com.faforever.client.theme.UiService; +import com.faforever.client.util.RatingUtil; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +@Slf4j +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor + +public class TeamMatchmakingMapListController extends NodeController { + + private static final int TILE_SIZE = 125; + private static final int PADDING = 20; + + private static final Comparator MAP_VERSION_COMPARATOR = Comparator.nullsFirst( + Comparator.comparing(MapVersion::size)) + .thenComparing( + mapVersion -> mapVersion.map() + .displayName(), + String.CASE_INSENSITIVE_ORDER); + + private static final Comparator MAP_POOL_COMPARATOR = Comparator.comparing( + MatchmakerQueueMapPool::minRating, Comparator.nullsFirst(Double::compare)) + .thenComparing( + MatchmakerQueueMapPool::maxRating, + Comparator.nullsLast( + Double::compare)); + + + private final MapService mapService; + private final UiService uiService; + private final PlayerService playerService; + private final FxApplicationThreadExecutor fxApplicationThreadExecutor; + + private final DoubleProperty maxWidth = new SimpleDoubleProperty(0); + private final DoubleProperty maxHeight = new SimpleDoubleProperty(0); + private final ObjectProperty>> brackets = new SimpleObjectProperty<>( + Map.of()); + private final IntegerProperty playerRating = new SimpleIntegerProperty(); + private final ObjectProperty queue = new SimpleObjectProperty<>(); + + private final ObservableValue> sortedMapPools = brackets.map(this::getSortedMapPools) + .orElse(List.of()); + private final ObservableValue> sortedMaps = brackets.map(this::getSortedMaps).orElse(List.of()); + private final ObservableValue playerBracketIndex = Bindings.createObjectBinding(this::calculateBracketIndex, + sortedMapPools, + playerRating); + + public Pane root; + public FlowPane tilesContainer; + public ScrollPane scrollContainer; + public VBox loadingPane; + + @Override + protected void onInitialize() { + this.bindProperties(); + } + + private void bindProperties() { + JavaFxUtil.bindManagedToVisible(loadingPane); + + this.queue.when(showing).subscribe(value -> { + if (value == null) { + return; + } + loadingPane.setVisible(true); + mapService.getMatchmakerBrackets(value).subscribe(rawBrackets -> { + loadingPane.setVisible(false); + this.brackets.set(rawBrackets); + }); + }); + + playerRating.bind(playerService.currentPlayerProperty() + .flatMap(player -> queue.flatMap(MatchmakerQueueInfo::leaderboardProperty) + .map(leaderboard -> RatingUtil.getLeaderboardRating(player, + leaderboard))) + .orElse(0) + .when(showing)); + + this.maxWidth.when(showing).subscribe(this::resizeToContent); + this.maxHeight.when(showing).subscribe(this::resizeToContent); + this.sortedMaps.when(showing).subscribe(this::updateContent); + this.playerBracketIndex.when(showing).subscribe(this::updateContent); + } + + @Override + public Pane getRoot() { + return root; + } + + public double getMaxWidth() { + return this.maxWidth.get(); + } + + public void setMaxWidth(double value) { + this.maxWidth.set(value); + } + + public DoubleProperty maxWidthProperty() { + return this.maxWidth; + } + + public double getMaxHeight() { + return this.maxHeight.get(); + } + + public void setMaxHeight(double value) { + this.maxHeight.set(value); + } + + public DoubleProperty maxHeightProperty() { + return this.maxHeight; + } + + public MatchmakerQueueInfo getQueue() { + return this.queue.get(); + } + + public void setQueue(MatchmakerQueueInfo queue) { + this.queue.set(queue); + } + + public ObjectProperty queueProperty() { + return this.queue; + } + + private List getSortedMapPools(Map> brackets) { + return brackets.keySet().stream().sorted(MAP_POOL_COMPARATOR).toList(); + } + + private List getSortedMaps(Map> map) { + return map.entrySet() + .stream() + .sorted(Entry.comparingByKey(MAP_POOL_COMPARATOR)) + .flatMap(entry -> entry.getValue().stream().sorted(MAP_VERSION_COMPARATOR)) + .distinct() + .toList(); + } + + private Integer calculateBracketIndex() { + int rating = playerRating.get(); + List pools = this.sortedMapPools.getValue(); + return pools.stream() + .filter(pool -> pool.minRating() == null || pool.minRating() < rating) + .filter(pool -> pool.maxRating() == null || pool.maxRating() > rating) + .findFirst() + .map(pools::indexOf) + .orElse(null); + } + + private Pane createMapTile(MapVersion mapVersion) { + Integer playerBracketIndex = this.playerBracketIndex.getValue(); + List pools = this.sortedMapPools.getValue(); + Map> brackets = this.brackets.get(); + double relevanceLevel = 1; + if (playerBracketIndex != null) { + int bracketDistance = brackets.entrySet() + .stream() + .filter(entry -> entry.getValue().contains(mapVersion)) + .map(Entry::getKey) + .mapToInt(pools::indexOf) + .filter(index -> index >= 0) + .map(index -> Math.abs(index - playerBracketIndex)) + .min() + .orElseThrow(); + + relevanceLevel = switch (bracketDistance) { + case 0 -> 1; + case 1 -> 0.2; + default -> 0; + }; + } + + TeamMatchmakingMapTileController controller = uiService.loadFxml( + "theme/play/teammatchmaking/matchmaking_map_tile.fxml"); + controller.setRelevanceLevel(relevanceLevel); + controller.setMapVersion(mapVersion); + return controller.getRoot(); + } + + private void resizeToContent() { + int tilecount = this.sortedMaps.getValue().size(); + if (tilecount == 0) { + return; + } + + double hgap = tilesContainer.getHgap(); + double vgap = tilesContainer.getVgap(); + + double tileHSize = TILE_SIZE + hgap; + double tileVSize = TILE_SIZE + vgap; + + int maxTilesInLine = (int) Math.min(10, Math.floor((getMaxWidth() * 0.95 - PADDING * 2 + hgap) / tileHSize)); + int maxLinesWithoutScroll = (int) Math.floor((getMaxHeight() * 0.95 - PADDING * 2 + vgap) / tileVSize); + + double maxScrollPaneHeight = maxLinesWithoutScroll * tileVSize - vgap; + this.scrollContainer.setMaxHeight(maxScrollPaneHeight); + + int tilesInOneLine = Math.min(maxTilesInLine, + Math.max(Math.max(4, Math.ceilDiv(tilecount, Math.max(1, maxLinesWithoutScroll))), + (int) Math.ceil(Math.sqrt(tilecount)))); + int numberOfLines = Math.ceilDiv(tilecount, tilesInOneLine); + + double preferredWidth = tileHSize * tilesInOneLine - hgap; + double gridHeight = tileVSize * numberOfLines - vgap; + + if (gridHeight > maxScrollPaneHeight) { + int scrollWidth = 18; + scrollContainer.setPrefWidth(preferredWidth + scrollWidth); + scrollContainer.setPrefHeight(maxScrollPaneHeight); + } else { + scrollContainer.setPrefWidth(preferredWidth); + scrollContainer.setPrefHeight(gridHeight); + } + + tilesContainer.setPrefWidth(preferredWidth); + } + + private void updateContent() { + List mapTiles = sortedMaps.getValue().stream().map(this::createMapTile).toList(); + + fxApplicationThreadExecutor.execute(() -> { + this.tilesContainer.getChildren().setAll(mapTiles); + this.resizeToContent(); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapTileController.java b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapTileController.java new file mode 100644 index 0000000000..44864e5715 --- /dev/null +++ b/src/main/java/com/faforever/client/teammatchmaking/TeamMatchmakingMapTileController.java @@ -0,0 +1,101 @@ +package com.faforever.client.teammatchmaking; + +import com.faforever.client.domain.api.Map; +import com.faforever.client.domain.api.MapVersion; +import com.faforever.client.fx.ImageViewHelper; +import com.faforever.client.fx.NodeController; +import com.faforever.client.i18n.I18n; +import com.faforever.client.map.MapService; +import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.generator.MapGeneratorService; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.Label; +import javafx.scene.effect.ColorAdjust; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor + +public class TeamMatchmakingMapTileController extends NodeController { + + private final MapService mapService; + private final I18n i18n; + private final ImageViewHelper imageViewHelper; + private final MapGeneratorService mapGeneratorService; + + public Pane root; + public ImageView thumbnailImageView; + public Label nameLabel; + public Label authorLabel; + public Label sizeLabel; + public VBox authorBox; + + protected final ObjectProperty entity = new SimpleObjectProperty<>(); + private DoubleProperty relevanceLevel = new SimpleDoubleProperty(0);; + + public double getRelevanceLevel(){ + return this.relevanceLevel.get(); + } + public void setRelevanceLevel(double value) { + this.relevanceLevel.set(value); + } + public DoubleProperty relevanceLevelProperty() { + return this.relevanceLevel; + } + + @Override + public Pane getRoot() { + return root; + } + + + public void setMapVersion(MapVersion mapVersion) { + this.entity.set(mapVersion); + } + + + @Override + protected void onInitialize(){ + thumbnailImageView.imageProperty().bind(entity.map(mapVersionBean -> mapService.loadPreview(mapVersionBean, PreviewSize.SMALL)) + .flatMap(imageViewHelper::createPlaceholderImageOnErrorObservable)); + thumbnailImageView.effectProperty().bind(relevanceLevel.map(relevanceLevel -> { + ColorAdjust grayscaleEffect = new ColorAdjust(); + grayscaleEffect.setSaturation(-1 + relevanceLevel.intValue()); + return grayscaleEffect; + })); + ObservableValue mapObservable = entity.map(MapVersion::map); + + nameLabel.textProperty().bind(mapObservable.map(map -> { + String name = map.displayName(); + if (mapGeneratorService.isGeneratedMap(name)) { + return "map generator"; + } + return name; + })); + + authorBox.visibleProperty().bind(mapObservable.map(map -> (map.author() != null) || (mapGeneratorService.isGeneratedMap(map.displayName())))); + authorLabel.textProperty().bind(mapObservable.map(map -> { + if (map.author() != null) { + return map.author().getUsername(); + } else if (mapGeneratorService.isGeneratedMap(map.displayName())) { + return "Neroxis"; + } else { + return i18n.get("map.unknownAuthor"); + } + })); + sizeLabel.textProperty().bind(entity.map(MapVersion::size).map(size -> i18n.get("mapPreview.size", size.widthInKm(), size.heightInKm()))); + } +} \ No newline at end of file diff --git a/src/main/java/com/faforever/client/theme/UiService.java b/src/main/java/com/faforever/client/theme/UiService.java index 1bbbebcf15..8b34e41cf4 100644 --- a/src/main/java/com/faforever/client/theme/UiService.java +++ b/src/main/java/com/faforever/client/theme/UiService.java @@ -103,8 +103,12 @@ public Dialog showInDialog(StackPane parent, Node content) { return showInDialog(parent, content, null); } - public Dialog showInDialog(StackPane parent, Node content, String title) { - DialogLayout dialogLayout = new DialogLayout(); + public Dialog showInDialog(StackPane parent, Node content, String title) { return showInDialog(parent, content, title, false); } + + public Dialog showInDialog(StackPane parent, Node content, String title, boolean basicMode) { return showInDialog(parent, content, title, basicMode, DialogTransition.TOP); } + + public Dialog showInDialog(StackPane parent, Node content, String title, boolean basicMode, DialogTransition transition) { + DialogLayout dialogLayout = new DialogLayout(basicMode); if (title != null) { dialogLayout.setHeading(new Label(title)); } @@ -112,7 +116,7 @@ public Dialog showInDialog(StackPane parent, Node content, String title) { Dialog dialog = new Dialog(); dialog.setContent(dialogLayout); - dialog.setTransitionType(DialogTransition.TOP); + dialog.setTransitionType(transition); parent.setOnKeyPressed(event -> { if (event.getCode() == KeyCode.ESCAPE) { diff --git a/src/main/java/com/faforever/client/ui/dialog/DialogLayout.java b/src/main/java/com/faforever/client/ui/dialog/DialogLayout.java index bac34d7833..54dcb543cc 100644 --- a/src/main/java/com/faforever/client/ui/dialog/DialogLayout.java +++ b/src/main/java/com/faforever/client/ui/dialog/DialogLayout.java @@ -12,18 +12,30 @@ /** Ported from JFoenix since we wanted to get rid of the JFoenix dependency */ public class DialogLayout extends VBox { private static final String DEFAULT_STYLE_CLASS = "dialog-layout"; + private static final String BASIC_MODE_STYLE_CLASS = "dialog-layout_basic"; private final StackPane heading = new StackPane(); private final StackPane body = new StackPane(); private final FlowPane actions = new FlowPane(); public DialogLayout() { + this(false); + } + + public DialogLayout(boolean basicMode) { initialize(); - heading.getStyleClass().addAll("layout-heading", "title"); + body.getStyleClass().add("layout-body"); VBox.setVgrow(body, Priority.ALWAYS); - actions.getStyleClass().add("layout-actions"); - getChildren().setAll(heading, body, actions); + + if (basicMode) { + this.getStyleClass().add(BASIC_MODE_STYLE_CLASS); + getChildren().setAll(body); + } else { + heading.getStyleClass().addAll("layout-heading", "title"); + actions.getStyleClass().add("layout-actions"); + getChildren().setAll(heading, body, actions); + } } public ObservableList getHeading() { diff --git a/src/main/java/com/faforever/client/vault/VaultEntityController.java b/src/main/java/com/faforever/client/vault/VaultEntityController.java index e01d44a18c..f7714e4688 100644 --- a/src/main/java/com/faforever/client/vault/VaultEntityController.java +++ b/src/main/java/com/faforever/client/vault/VaultEntityController.java @@ -351,7 +351,7 @@ protected enum State { } public enum SearchType { - SEARCH, OWN, NEWEST, HIGHEST_RATED, PLAYER, RECOMMENDED, MAP_POOL, PLAYED, HIGHEST_RATED_UI + SEARCH, OWN, NEWEST, HIGHEST_RATED, PLAYER, RECOMMENDED, PLAYED, HIGHEST_RATED_UI } public record ShowRoomCategory( diff --git a/src/main/resources/css/controls/dialog-layout.css b/src/main/resources/css/controls/dialog-layout.css index d80ca72293..c625465b17 100644 --- a/src/main/resources/css/controls/dialog-layout.css +++ b/src/main/resources/css/controls/dialog-layout.css @@ -20,3 +20,7 @@ .dialog-layout > .layout-body .label { -fx-wrap-text: true; } + +.dialog-layout_basic > .layout-body { + -fx-padding: 0 0 0 0; +} diff --git a/src/main/resources/theme/play/teammatchmaking/matchmaking_map_tile.fxml b/src/main/resources/theme/play/teammatchmaking/matchmaking_map_tile.fxml new file mode 100644 index 0000000000..e4b6a8debf --- /dev/null +++ b/src/main/resources/theme/play/teammatchmaking/matchmaking_map_tile.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/theme/play/teammatchmaking/matchmaking_maplist_popup.fxml b/src/main/resources/theme/play/teammatchmaking/matchmaking_maplist_popup.fxml new file mode 100644 index 0000000000..60f65a5e62 --- /dev/null +++ b/src/main/resources/theme/play/teammatchmaking/matchmaking_maplist_popup.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/theme/style.css b/src/main/resources/theme/style.css index a56ac71b39..e7a6144719 100644 --- a/src/main/resources/theme/style.css +++ b/src/main/resources/theme/style.css @@ -1095,6 +1095,20 @@ -fx-opacity: 0; } +.image-dimmer-light { + -fx-padding: 0 3 3 3; + -fx-background-color: linear-gradient(to bottom, #00000000 0%, #00000022 10%, #00000033 20%, #0000004d 30%, #00000066 40%, #00000080 50%, #00000099 60%, #000000b3 70%); +} + +.image-dimmer-light-reversed { + -fx-padding: 3 3 0 3; + -fx-background-color: linear-gradient(to top, #00000000 0%, #00000000 10%, #00000033 20%, #0000004d 30%, #00000066 40%, #00000080 50%, #00000099 60%, #000000b3 70%); +} + +.tmm-maplist { + -fx-padding: 20 20 20 20; //related to PADDING constant, do not change without adjusting it +} + /***************** Faction buttons *****************/ diff --git a/src/test/java/com/faforever/client/map/MapServiceTest.java b/src/test/java/com/faforever/client/map/MapServiceTest.java index 2803e6e72b..8d393d10b9 100644 --- a/src/test/java/com/faforever/client/map/MapServiceTest.java +++ b/src/test/java/com/faforever/client/map/MapServiceTest.java @@ -4,7 +4,6 @@ import com.faforever.client.builders.MatchmakerQueueInfoBuilder; import com.faforever.client.builders.PlayerInfoBuilder; import com.faforever.client.config.ClientProperties; -import com.faforever.client.domain.api.Map; import com.faforever.client.domain.api.MapPoolAssignment; import com.faforever.client.domain.api.MapVersion; import com.faforever.client.domain.server.MatchmakerQueueInfo; @@ -73,7 +72,6 @@ import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.core.Is.is; import static org.instancio.Select.field; -import static org.instancio.Select.scope; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -150,7 +148,7 @@ public void setUp() throws Exception { }).when(taskService).submitTask(any()); instance = new MapService(notificationService, taskService, fafApiAccessor, assetService, i18n, - themeService, mapGeneratorService, playerService, mapMapper, fileSizeReader, + themeService, mapGeneratorService, playerService, mapMapper, matchmakerMapper, fileSizeReader, clientProperties, forgedAlliancePrefs, preferences, mapUploadTaskFactory, downloadMapTaskFactory, uninstallMapTaskFactory, fxApplicationThreadExecutor); instance.officialMaps = Set.of(); @@ -442,55 +440,17 @@ public void testGetMatchMakerMaps() throws Exception { Flux resultFlux = Flux.fromIterable( matchmakerMapper.mapAssignmentBeans(List.of(mapPoolAssignment1, mapPoolAssignment2))); - when(fafApiAccessor.getMany(any(), anyString())).thenReturn(resultFlux); - when(playerService.getCurrentPlayer()).thenReturn(PlayerInfoBuilder.create().defaultValues().get()); + when(fafApiAccessor.getMany(any())).thenReturn(resultFlux); MatchmakerQueueInfo matchmakerQueue = MatchmakerQueueInfoBuilder.create().defaultValues().get(); - StepVerifier.create(instance.getMatchmakerMapsWithPageCount(matchmakerQueue, 10, 1)).assertNext(results -> { - assertThat(results.getT1(), hasSize(2)); - assertThat(results.getT2(), is(1)); + StepVerifier.create(instance.getMatchmakerBrackets(matchmakerQueue)).assertNext(results -> { + assertThat(results.entrySet(), hasSize(2)); }).verifyComplete(); verify(fafApiAccessor).getMany( - argThat(ElideMatchers.hasDtoClass(com.faforever.commons.api.dto.MapPoolAssignment.class)), anyString()); + argThat(ElideMatchers.hasDtoClass(com.faforever.commons.api.dto.MapPoolAssignment.class))); } - @Test - public void testGetMatchMakerMapsWithPagination() throws Exception { - MapPoolAssignment mapPoolAssignment1 = Instancio.of(MapPoolAssignment.class) - .set(field(MapVersion::size).within(scope(MapVersion.class)), - new MapSize(512, 512)) - .set(field(Map::displayName).within(scope(Map.class)), - "a") - .create(); - MapPoolAssignment mapPoolAssignment2 = Instancio.of(MapPoolAssignment.class) - .set(field(MapVersion::size).within(scope(MapVersion.class)), - new MapSize(512, 512)) - .set(field(Map::displayName).within(scope(Map.class)), - "b") - .create(); - MapPoolAssignment mapPoolAssignment3 = Instancio.of(MapPoolAssignment.class) - .set(field(MapVersion::size).within(scope(MapVersion.class)), - new MapSize(1024, 1024)) - .set(field(Map::displayName).within(scope(Map.class)), - "c") - .create(); - - Flux resultFlux = Flux.fromIterable( - matchmakerMapper.mapAssignmentBeans(List.of(mapPoolAssignment1, mapPoolAssignment2, mapPoolAssignment3))); - when(fafApiAccessor.getMany(any(), anyString())).thenReturn(resultFlux); - when(playerService.getCurrentPlayer()).thenReturn(PlayerInfoBuilder.create().defaultValues().get()); - - MatchmakerQueueInfo matchmakerQueue = MatchmakerQueueInfoBuilder.create().defaultValues().get(); - StepVerifier.create(instance.getMatchmakerMapsWithPageCount(matchmakerQueue, 1, 2)).assertNext(results -> { - assertThat(results.getT1(), hasSize(1)); - assertThat(results.getT1().getFirst().id(), is(mapPoolAssignment2.mapVersion().id())); - assertThat(results.getT2(), is(3)); - }).verifyComplete(); - - verify(fafApiAccessor).getMany( - argThat(ElideMatchers.hasDtoClass(com.faforever.commons.api.dto.MapPoolAssignment.class)), anyString()); - } @Test public void testHasPlayedMap() throws Exception { diff --git a/src/test/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemControllerTest.java b/src/test/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemControllerTest.java index 1aadef5543..9e8a03d3a7 100644 --- a/src/test/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemControllerTest.java +++ b/src/test/java/com/faforever/client/teammatchmaking/MatchmakingQueueItemControllerTest.java @@ -94,13 +94,6 @@ public void testQueueNameSet() { assertThat(instance.selectButton.getText(), is(queue.getTechnicalName())); } - @Test - public void testShowMapPool() { - instance.showMapPool(); - - verify(navigationHandler).navigateTo(any(ShowMapPoolEvent.class)); - } - @Test public void testOnJoinLeaveQueueButtonClicked() { runOnFxThreadAndWait(() -> instance.selectButton.fire());