package com.faforever.client.main; import com.faforever.client.cast.CastsController; import com.faforever.client.chat.ChatController; import com.faforever.client.chat.ChatService; import com.faforever.client.chat.PlayerInfoBean; import com.faforever.client.connectivity.ConnectivityService; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.WindowController; import com.faforever.client.game.Faction; import com.faforever.client.game.GameService; import com.faforever.client.game.GamesController; import com.faforever.client.hub.CommunityHubController; import com.faforever.client.i18n.I18n; import com.faforever.client.leaderboard.LeaderboardController; import com.faforever.client.login.LoginController; import com.faforever.client.map.MapVaultController; import com.faforever.client.mod.ModVaultController; import com.faforever.client.news.NewsController; import com.faforever.client.news.UnreadNewsEvent; import com.faforever.client.notification.ImmediateNotification; import com.faforever.client.notification.ImmediateNotificationController; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.PersistentNotification; import com.faforever.client.notification.PersistentNotificationsController; import com.faforever.client.notification.Severity; import com.faforever.client.notification.TransientNotification; import com.faforever.client.notification.TransientNotificationsController; import com.faforever.client.os.OperatingSystem; import com.faforever.client.patch.GameUpdateService; import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.OnChooseGameDirectoryListener; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.preferences.WindowPrefs; import com.faforever.client.preferences.ui.SettingsController; import com.faforever.client.rankedmatch.MatchmakerMessage; import com.faforever.client.rankedmatch.Ranked1v1Controller; import com.faforever.client.remote.FafService; import com.faforever.client.remote.domain.RatingRange; import com.faforever.client.replay.ReplayVaultController; import com.faforever.client.task.TaskService; import com.faforever.client.theme.ThemeService; import com.faforever.client.units.UnitsController; import com.faforever.client.update.ClientUpdateService; import com.faforever.client.user.UserService; import com.faforever.client.util.IdenticonUtil; import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.css.PseudoClass; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.fxml.FXML; import javafx.geometry.Bounds; import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.Labeled; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressBar; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.SplitMenuButton; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.stage.DirectoryChooser; import javafx.stage.Modality; import javafx.stage.Popup; import javafx.stage.PopupWindow; import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.StageStyle; import org.bridj.Pointer; import org.bridj.PointerIO; import org.bridj.cpp.com.COMRuntime; import org.bridj.cpp.com.shell.ITaskbarList3; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.File; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ThreadPoolExecutor; import java.util.function.Function; import static com.faforever.client.fx.WindowController.WindowButtonType.CLOSE; import static com.faforever.client.fx.WindowController.WindowButtonType.MAXIMIZE_RESTORE; import static com.faforever.client.fx.WindowController.WindowButtonType.MINIMIZE; import static com.faforever.client.os.OperatingSystem.WINDOWS; import static com.github.nocatch.NoCatch.noCatch; // TODO divide and conquer public class MainController implements OnChooseGameDirectoryListener { private static final PseudoClass NOTIFICATION_INFO_PSEUDO_CLASS = PseudoClass.getPseudoClass("info"); private static final PseudoClass NOTIFICATION_WARN_PSEUDO_CLASS = PseudoClass.getPseudoClass("warn"); private static final PseudoClass NOTIFICATION_ERROR_PSEUDO_CLASS = PseudoClass.getPseudoClass("error"); private static final PseudoClass NAVIGATION_ACTIVE_PSEUDO_CLASS = PseudoClass.getPseudoClass("active"); private static final PseudoClass CONNECTIVITY_PUBLIC_PSEUDO_CLASS = PseudoClass.getPseudoClass("public"); private static final PseudoClass CONNECTIVITY_STUN_PSEUDO_CLASS = PseudoClass.getPseudoClass("stun"); private static final PseudoClass CONNECTIVITY_BLOCKED_PSEUDO_CLASS = PseudoClass.getPseudoClass("blocked"); private static final PseudoClass CONNECTIVITY_UNKNOWN_PSEUDO_CLASS = PseudoClass.getPseudoClass("unknown"); private static final PseudoClass CONNECTIVITY_CONNECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("connected"); private static final PseudoClass CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("disconnected"); private static final PseudoClass HIGHLIGHTED = PseudoClass.getPseudoClass("highlighted"); @FXML Label chatConnectionStatusIcon; @FXML Label fafConnectionStatusIcon; @FXML Label portCheckStatusIcon; @FXML ImageView userImageView; @FXML HBox mainNavigation; @FXML Pane mainHeaderPane; @FXML ButtonBase notificationsButton; @FXML Pane contentPane; @FXML SplitMenuButton newsButton; @FXML SplitMenuButton chatButton; @FXML SplitMenuButton playButton; @FXML SplitMenuButton vaultButton; @FXML SplitMenuButton leaderboardButton; @FXML ProgressBar taskProgressBar; @FXML Pane mainRoot; @FXML Button usernameButton; @FXML Pane taskPane; @FXML Labeled portCheckStatusButton; @FXML MenuButton fafConnectionButton; @FXML MenuButton chatConnectionButton; @FXML Label taskProgressLabel; @Resource NewsController newsController; @Resource ChatController chatController; @Resource UnitsController unitsController; @Resource GamesController gamesController; @Resource Ranked1v1Controller ranked1v1Controller; @Resource LeaderboardController leaderboardController; @Resource ReplayVaultController replayVaultController; @Resource PersistentNotificationsController persistentNotificationsController; @Resource TransientNotificationsController transientNotificationsController; @Resource PreferencesService preferencesService; @Resource ConnectivityService connectivityService; @Resource I18n i18n; @Resource UserService userService; @Resource TaskService taskService; @Resource NotificationService notificationService; @Resource SettingsController settingsController; @Resource ApplicationContext applicationContext; @Resource PlayerService playerService; @Resource ModVaultController modVaultController; @Resource MapVaultController mapMapVaultController; @Resource CastsController castsController; @Resource CommunityHubController communityHubController; @Resource GameUpdateService gameUpdateService; @Resource GameService gameService; @Resource ClientUpdateService clientUpdateService; @Resource UserMenuController userMenuController; @Resource Stage stage; @Resource Locale locale; @Resource LoginController loginController; @Resource FafService fafService; @Resource ChatService chatService; @Resource ThreadPoolExecutor threadPoolExecutor; @Resource WindowController windowController; @Resource ThemeService themeService; @Resource EventBus eventBus; @Value("${mainWindowTitle}") String mainWindowTitle; @Value("${rating.beta}") int ratingBeta; @VisibleForTesting Popup persistentNotificationsPopup; private Popup userMenuPopup; private ChangeListener<Boolean> windowFocusListener; private Popup transientNotificationsPopup; private ITaskbarList3 taskBarList; private Pointer<Integer> taskBarRelatedPointer; @FXML void initialize() { taskPane.managedProperty().bind(taskPane.visibleProperty()); taskProgressBar.managedProperty().bind(taskProgressBar.visibleProperty()); taskProgressLabel.managedProperty().bind(taskProgressLabel.visibleProperty()); addHoverListener(playButton); addHoverListener(newsButton); addHoverListener(chatButton); addHoverListener(vaultButton); addHoverListener(leaderboardButton); setCurrentTaskInStatusBar(null); windowFocusListener = (observable, oldValue, newValue) -> { if (!newValue) { hideAllMenuDropdowns(); } }; } private void addHoverListener(SplitMenuButton button) { button.hoverProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { showMenuDropdown(button); } }); } /** * @param task the task to set, {@code null} to unset */ private void setCurrentTaskInStatusBar(Task<?> task) { Platform.runLater(() -> { if (task == null) { taskProgressBar.setVisible(false); taskProgressLabel.setVisible(false); updateTaskbarProgress(null); return; } taskProgressBar.setVisible(true); taskProgressBar.progressProperty().bind(task.progressProperty()); taskProgressLabel.setVisible(true); taskProgressLabel.textProperty().bind(task.titleProperty()); updateTaskbarProgress(ProgressIndicator.INDETERMINATE_PROGRESS); task.progressProperty().addListener((observable, oldValue, newValue) -> { updateTaskbarProgress(newValue.doubleValue()); }); }); } private void hideAllMenuDropdowns() { mainNavigation.getChildren().forEach(item -> ((SplitMenuButton) item).hide()); } private void showMenuDropdown(SplitMenuButton button) { mainNavigation.getChildren().stream() .filter(item -> item instanceof SplitMenuButton && item != button) .forEach(item -> ((SplitMenuButton) item).hide()); button.show(); } /** * Updates the progress in the Windows 7+ task bar, if available. */ @SuppressWarnings("unchecked") private void updateTaskbarProgress(@Nullable Double progress) { if (taskBarRelatedPointer == null || taskBarList == null) { return; } threadPoolExecutor.execute(() -> { if (taskBarList == null) { return; } if (progress == null) { taskBarList.SetProgressState(taskBarRelatedPointer, ITaskbarList3.TbpFlag.TBPF_NOPROGRESS); } else if (progress == ProgressIndicator.INDETERMINATE_PROGRESS) { taskBarList.SetProgressState(taskBarRelatedPointer, ITaskbarList3.TbpFlag.TBPF_INDETERMINATE); } else { taskBarList.SetProgressState(taskBarRelatedPointer, ITaskbarList3.TbpFlag.TBPF_NORMAL); taskBarList.SetProgressValue(taskBarRelatedPointer, (int) (progress * 100), 100); } }); } @PostConstruct void postConstruct() { eventBus.register(this); // We need to initialize all skins, so initially add the chat root to the scene graph. setContent(chatController.getRoot()); chatService.unreadMessagesCount().addListener((observable, oldValue, newValue) -> { themeService.setApplicationIconBadgeNumber(stage, newValue.intValue()); }); fafService.connectionStateProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { switch (newValue) { case DISCONNECTED: fafConnectionButton.setText(i18n.get("statusBar.fafDisconnected")); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_CONNECTED_PSEUDO_CLASS, false); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS, true); break; case CONNECTING: fafConnectionButton.setText(i18n.get("statusBar.fafConnecting")); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_CONNECTED_PSEUDO_CLASS, false); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS, false); break; case CONNECTED: fafConnectionButton.setText(i18n.get("statusBar.fafConnected")); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_CONNECTED_PSEUDO_CLASS, true); fafConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS, false); break; } }); }); chatService.connectionStateProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { chatConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_CONNECTED_PSEUDO_CLASS, false); chatConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS, false); switch (newValue) { case DISCONNECTED: chatConnectionButton.setText(i18n.get("statusBar.chatDisconnected")); chatConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_DISCONNECTED_PSEUDO_CLASS, true); break; case CONNECTING: chatConnectionButton.setText(i18n.get("statusBar.chatConnecting")); break; case CONNECTED: chatConnectionButton.setText(i18n.get("statusBar.chatConnected")); chatConnectionStatusIcon.pseudoClassStateChanged(CONNECTIVITY_CONNECTED_PSEUDO_CLASS, true); break; } }); }); connectivityService.connectivityStateProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_PUBLIC_PSEUDO_CLASS, false); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_STUN_PSEUDO_CLASS, false); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_BLOCKED_PSEUDO_CLASS, false); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_UNKNOWN_PSEUDO_CLASS, false); switch (newValue) { case PUBLIC: portCheckStatusIcon.setText("\uF111"); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_PUBLIC_PSEUDO_CLASS, true); portCheckStatusButton.setText(i18n.get("statusBar.connectivityPublic")); break; case STUN: portCheckStatusIcon.setText("\uF06A"); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_STUN_PSEUDO_CLASS, true); portCheckStatusButton.setText(i18n.get("statusBar.connectivityStun")); break; case BLOCKED: portCheckStatusIcon.setText("\uF056"); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_BLOCKED_PSEUDO_CLASS, true); portCheckStatusButton.setText(i18n.get("statusBar.portUnreachable")); break; case RUNNING: portCheckStatusIcon.setText("\uF059"); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_UNKNOWN_PSEUDO_CLASS, true); portCheckStatusButton.setText(i18n.get("statusBar.checkingPort")); break; case UNKNOWN: portCheckStatusIcon.setText("\uF059"); portCheckStatusIcon.pseudoClassStateChanged(CONNECTIVITY_UNKNOWN_PSEUDO_CLASS, true); portCheckStatusButton.setText(i18n.get("statusBar.connectivityUnknown")); break; default: throw new AssertionError("Uncovered value: " + newValue); } }); }); persistentNotificationsPopup = new Popup(); persistentNotificationsPopup.getContent().setAll(persistentNotificationsController.getRoot()); persistentNotificationsPopup.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT); persistentNotificationsPopup.setAutoFix(false); persistentNotificationsPopup.setAutoHide(true); transientNotificationsPopup = new Popup(); transientNotificationsPopup.getScene().getRoot().getStyleClass().add("transient-notification"); transientNotificationsPopup.getContent().setAll(transientNotificationsController.getRoot()); transientNotificationsPopup.anchorLocationProperty().bind(Bindings.createObjectBinding(() -> { switch (preferencesService.getPreferences().getNotification().getToastPosition()) { case TOP_RIGHT: return PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT; case BOTTOM_LEFT: return PopupWindow.AnchorLocation.CONTENT_BOTTOM_LEFT; case TOP_LEFT: return PopupWindow.AnchorLocation.CONTENT_TOP_LEFT; default: return PopupWindow.AnchorLocation.CONTENT_BOTTOM_RIGHT; } }, preferencesService.getPreferences().getNotification().toastPositionProperty() )); transientNotificationsController.getRoot().getChildren().addListener((InvalidationListener) observable -> { boolean enabled = preferencesService.getPreferences().getNotification().isTransientNotificationsEnabled(); if (!transientNotificationsController.getRoot().getChildren().isEmpty() && enabled) { Rectangle2D visualBounds = getTransientNotificationAreaBounds(); transientNotificationsPopup.show(mainRoot.getScene().getWindow(), visualBounds.getMaxX(), visualBounds.getMaxY()); } else { transientNotificationsPopup.hide(); } }); userMenuPopup = new Popup(); userMenuPopup.setAutoFix(false); userMenuPopup.setAutoHide(true); userMenuPopup.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_TOP_RIGHT); userMenuPopup.getContent().setAll(userMenuController.getRoot()); notificationService.addPersistentNotificationListener( change -> Platform.runLater(() -> updateNotificationsButton(change.getSet())) ); notificationService.addImmediateNotificationListener( notification -> Platform.runLater(() -> displayImmediateNotification(notification)) ); notificationService.addTransientNotificationListener( notification -> Platform.runLater(() -> transientNotificationsController.addNotification(notification)) ); taskService.getActiveTasks().addListener((Observable observable) -> { Collection<Task<?>> runningTasks = taskService.getActiveTasks(); if (runningTasks.isEmpty()) { setCurrentTaskInStatusBar(null); } else { setCurrentTaskInStatusBar(runningTasks.iterator().next()); } }); preferencesService.getPreferences().getForgedAlliance().portProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> i18n.get("statusBar.portCheckTooltip", newValue)); }); preferencesService.setOnChooseGameDirectoryListener(this); gameService.addOnRankedMatchNotificationListener(this::onMatchmakerMessage); userService.loggedInProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { onLoggedIn(); } else { onLoggedOut(); } }); } @Subscribe public void onUnreadNews(UnreadNewsEvent event) { Platform.runLater(() -> newsButton.pseudoClassStateChanged(HIGHLIGHTED, event.hasUnreadNews())); } private void setContent(Node node) { ObservableList<Node> children = contentPane.getChildren(); if (!children.contains(node)) { children.add(node); AnchorPane.setTopAnchor(node, 0d); AnchorPane.setRightAnchor(node, 0d); AnchorPane.setBottomAnchor(node, 0d); AnchorPane.setLeftAnchor(node, 0d); } for (Node child : children) { child.setVisible(child == node); } } private Rectangle2D getTransientNotificationAreaBounds() { ObservableList<Screen> screens = Screen.getScreens(); int toastScreenIndex = preferencesService.getPreferences().getNotification().getToastScreen(); Screen screen; if (toastScreenIndex < screens.size()) { screen = screens.get(Math.max(0, toastScreenIndex)); } else { screen = Screen.getPrimary(); } return screen.getVisualBounds(); } /** * Updates the number displayed in the notifications button and sets its CSS pseudo class based on the highest * notification {@code Severity} of all current notifications. */ private void updateNotificationsButton(Collection<? extends PersistentNotification> notifications) { JavaFxUtil.assertApplicationThread(); notificationsButton.setText(String.format(locale, "%d", notifications.size())); Severity highestSeverity = null; for (PersistentNotification notification : notifications) { if (highestSeverity == null || notification.getSeverity().compareTo(highestSeverity) > 0) { highestSeverity = notification.getSeverity(); } } notificationsButton.pseudoClassStateChanged(NOTIFICATION_INFO_PSEUDO_CLASS, highestSeverity == Severity.INFO); notificationsButton.pseudoClassStateChanged(NOTIFICATION_WARN_PSEUDO_CLASS, highestSeverity == Severity.WARN); notificationsButton.pseudoClassStateChanged(NOTIFICATION_ERROR_PSEUDO_CLASS, highestSeverity == Severity.ERROR); } private void displayImmediateNotification(ImmediateNotification notification) { ImmediateNotificationController controller = applicationContext.getBean(ImmediateNotificationController.class); controller.setNotification(notification); Stage userInfoWindow = new Stage(StageStyle.TRANSPARENT); userInfoWindow.initModality(Modality.NONE); userInfoWindow.initOwner(mainRoot.getScene().getWindow()); WindowController windowController = applicationContext.getBean(WindowController.class); windowController.configure(userInfoWindow, controller.getRoot(), true, CLOSE, MAXIMIZE_RESTORE); userInfoWindow.show(); } private void onLoggedIn() { Platform.runLater(this::enterLoggedInState); } private void onLoggedOut() { Platform.runLater(this::enterLoggedOutState); } private void onMatchmakerMessage(MatchmakerMessage message) { if (message.getQueues() == null || gameService.gameRunningProperty().get() || !preferencesService.getPreferences().getNotification().isRanked1v1ToastEnabled()) { return; } PlayerInfoBean currentPlayer = playerService.getCurrentPlayer(); int deviationFor80PercentQuality = (int) (ratingBeta / 2.5f); int deviationFor75PercentQuality = (int) (ratingBeta / 1.25f); float leaderboardRatingDeviation = currentPlayer.getLeaderboardRatingDeviation(); Function<MatchmakerMessage.MatchmakerQueue, List<RatingRange>> ratingRangesSupplier; if (leaderboardRatingDeviation <= deviationFor80PercentQuality) { ratingRangesSupplier = MatchmakerMessage.MatchmakerQueue::getBoundary80s; } else if (leaderboardRatingDeviation <= deviationFor75PercentQuality) { ratingRangesSupplier = MatchmakerMessage.MatchmakerQueue::getBoundary75s; } else { return; } float leaderboardRatingMean = currentPlayer.getLeaderboardRatingMean(); boolean showNotification = false; for (MatchmakerMessage.MatchmakerQueue matchmakerQueue : message.getQueues()) { if (!Objects.equals("ladder1v1", matchmakerQueue.getQueueName())) { continue; } List<RatingRange> ratingRanges = ratingRangesSupplier.apply(matchmakerQueue); for (RatingRange ratingRange : ratingRanges) { if (ratingRange.getMin() <= leaderboardRatingMean && leaderboardRatingMean <= ratingRange.getMax()) { showNotification = true; break; } } } if (!showNotification) { return; } notificationService.addNotification(new TransientNotification( i18n.get("ranked1v1.notification.title"), i18n.get("ranked1v1.notification.message"), themeService.getThemeImage(ThemeService.RANKED_1V1_IMAGE), this::onPlayRanked1v1Selected )); } public void display() { windowController.configure(stage, mainRoot, true, MINIMIZE, MAXIMIZE_RESTORE, CLOSE); final WindowPrefs mainWindowPrefs = preferencesService.getPreferences().getMainWindow(); double x = mainWindowPrefs.getX(); double y = mainWindowPrefs.getY(); int width = mainWindowPrefs.getWidth(); int height = mainWindowPrefs.getHeight(); stage.setWidth(width); stage.setHeight(height); stage.show(); if (OperatingSystem.current() == WINDOWS) { initWindowsTaskBar(); } enterLoggedOutState(); ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(x, y, width, height); if (screensForRectangle.isEmpty()) { JavaFxUtil.centerOnScreen(stage); } else { stage.setX(x); stage.setY(y); } if (mainWindowPrefs.getMaximized()) { WindowController.maximize(stage); } registerWindowListeners(); } /** * Initializes the Windows 7+ task bar. */ @SuppressWarnings("unchecked") private void initWindowsTaskBar() { try { threadPoolExecutor.execute(() -> noCatch(() -> taskBarList = COMRuntime.newInstance(ITaskbarList3.class)) ); long hwndVal = com.sun.glass.ui.Window.getWindows().get(0).getNativeWindow(); taskBarRelatedPointer = Pointer.pointerToAddress(hwndVal, (PointerIO) null); } catch (NoClassDefFoundError e) { taskBarRelatedPointer = null; } } private void enterLoggedOutState() { stage.setTitle(i18n.get("login.title")); windowController.setContent(loginController.getRoot()); loginController.display(); } private void registerWindowListeners() { final WindowPrefs mainWindowPrefs = preferencesService.getPreferences().getMainWindow(); stage.maximizedProperty().addListener((observable, oldValue, newValue) -> { if (!newValue) { stage.setWidth(mainWindowPrefs.getWidth()); stage.setHeight(mainWindowPrefs.getHeight()); stage.setX(mainWindowPrefs.getX()); stage.setY(mainWindowPrefs.getY()); } mainWindowPrefs.setMaximized(newValue); preferencesService.storeInBackground(); }); stage.heightProperty().addListener((observable, oldValue, newValue) -> { if (!stage.isMaximized()) { mainWindowPrefs.setHeight(newValue.intValue()); preferencesService.storeInBackground(); } }); stage.widthProperty().addListener((observable, oldValue, newValue) -> { if (!stage.isMaximized()) { mainWindowPrefs.setWidth(newValue.intValue()); preferencesService.storeInBackground(); } }); stage.xProperty().addListener(observable -> { if (!stage.isMaximized()) { mainWindowPrefs.setX(stage.getX()); preferencesService.storeInBackground(); } }); stage.yProperty().addListener(observable -> { if (!stage.isMaximized()) { mainWindowPrefs.setY(stage.getY()); preferencesService.storeInBackground(); } }); ReadOnlyBooleanProperty focusedProperty = stage.focusedProperty(); focusedProperty.removeListener(windowFocusListener); focusedProperty.addListener(windowFocusListener); } private void enterLoggedInState() { stage.setTitle(mainWindowTitle); windowController.setContent(mainRoot); gameUpdateService.checkForUpdateInBackground(); clientUpdateService.checkForUpdateInBackground(); restoreLastView(); usernameButton.setText(userService.getUsername()); // TODO no more e-mail address :( // userImageView.setImage(gravatarService.getGravatar(userService.getEmail())); userImageView.setImage(IdenticonUtil.createIdenticon(userService.getUid())); } private void restoreLastView() { final WindowPrefs mainWindowPrefs = preferencesService.getPreferences().getMainWindow(); String lastView = mainWindowPrefs.getLastView(); if (preferencesService.getPreferences().getRememberLastTab() && lastView != null) { mainNavigation.getChildren().stream() .filter(button -> button instanceof ButtonBase) .filter(button -> lastView.equals(button.getId())) .forEach(button -> ((ButtonBase) button).fire()); } else { newsButton.fire(); } } @FXML void onPortCheckHelpClicked() { // FIXME implement } @FXML void onChangePortClicked() { // FIXME implement } @FXML void onPortCheckRetryClicked() { connectivityService.checkConnectivity(); } @FXML void onFafReconnectClicked() { fafService.reconnect(); } @FXML void onChatReconnectClicked() { chatService.reconnect(); } @FXML void onNotificationsButtonClicked() { Bounds screenBounds = notificationsButton.localToScreen(notificationsButton.getBoundsInLocal()); persistentNotificationsPopup.show(notificationsButton.getScene().getWindow(), screenBounds.getMaxX(), screenBounds.getMaxY()); } @FXML void onSupportItemSelected() { // FIXME implement } @FXML void onSettingsItemSelected() { Stage stage = new Stage(StageStyle.UNDECORATED); stage.initOwner(mainRoot.getScene().getWindow()); WindowController windowController = applicationContext.getBean(WindowController.class); windowController.configure(stage, settingsController.getRoot(), true, CLOSE); stage.setTitle(i18n.get("settings.windowTitle")); stage.show(); } @FXML void onExitItemSelected() { Platform.exit(); } @Override public CompletionStage<Path> onChooseGameDirectory() { CompletableFuture<Path> future = new CompletableFuture<>(); Platform.runLater(() -> { DirectoryChooser directoryChooser = new DirectoryChooser(); directoryChooser.setTitle(i18n.get("missingGamePath.chooserTitle")); File result = directoryChooser.showDialog(getRoot().getScene().getWindow()); if (result == null) { future.complete(null); } else { future.complete(result.toPath()); } }); return future; } public Pane getRoot() { return mainRoot; } @FXML void onCommunitySelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); selectLastChildOrFirstItem(newsButton); } /** * Sets the specified button to active state. */ private void setActiveNavigationButton(ButtonBase button) { mainNavigation.getChildren().stream() .filter(navigationButton -> navigationButton instanceof ButtonBase && navigationButton != button) .forEach(navigationItem -> navigationItem.pseudoClassStateChanged(NAVIGATION_ACTIVE_PSEUDO_CLASS, false)); button.pseudoClassStateChanged(NAVIGATION_ACTIVE_PSEUDO_CLASS, true); preferencesService.getPreferences().getMainWindow().setLastView(button.getId()); preferencesService.storeInBackground(); } /** * Selects the previously selected child view for the given button. If no match was found (or there hasn't been any * previous selected view), the first item is selected. */ private void selectLastChildOrFirstItem(SplitMenuButton button) { String lastChildView = preferencesService.getPreferences().getMainWindow().getLastChildViews().get(button.getId()); if (lastChildView == null) { button.getItems().get(0).fire(); return; } button.getItems().stream() .filter(item -> item.getId().equals(lastChildView)) .forEach(MenuItem::fire); } @FXML void onVaultSelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); selectLastChildOrFirstItem(vaultButton); } @FXML void onLeaderboardSelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); selectLastChildOrFirstItem(leaderboardButton); } @FXML void onPlaySelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); selectLastChildOrFirstItem(playButton); } @FXML void onChatSelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); setContent(chatController.getRoot()); } @FXML void onUnitsSelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); unitsController.setUpIfNecessary(); setContent(unitsController.getRoot()); } @FXML void onCommunityHubSelected(ActionEvent event) { setContent(communityHubController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } /** * Sets the parent navigation button of the specified menu item as active. */ private void setActiveNavigationButtonFromChild(MenuItem menuItem) { ChangeListener<Node> ownerNodeChangeListener = new ChangeListener<Node>() { @Override public void changed(ObservableValue<? extends Node> observable, Node oldValue, Node newValue) { setActiveNavigationButton((ButtonBase) newValue); preferencesService.getPreferences().getMainWindow().getLastChildViews().put(newValue.getId(), menuItem.getId()); preferencesService.storeInBackground(); observable.removeListener(this); } }; ChangeListener<ContextMenu> parentPopupChangeListener = new ChangeListener<ContextMenu>() { @Override public void changed(ObservableValue<? extends ContextMenu> observable, ContextMenu oldValue, ContextMenu newValue) { newValue.ownerNodeProperty().addListener(ownerNodeChangeListener); observable.removeListener(this); } }; menuItem.parentPopupProperty().addListener(parentPopupChangeListener); } @FXML void onNewsSelected(ActionEvent event) { setActiveNavigationButton((ButtonBase) event.getSource()); newsController.setUpIfNecessary(); setContent(newsController.getRoot()); } @FXML void onCastsSelected(ActionEvent event) { setContent(castsController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onPlayCustomSelected(ActionEvent event) { setContent(gamesController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onPlayRanked1v1Selected(Event event) { ranked1v1Controller.setUpIfNecessary(); setContent(ranked1v1Controller.getRoot()); // FIXME remove with if (event.getTarget() instanceof MenuItem) { setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } } @FXML void onMapsSelected(ActionEvent event) { mapMapVaultController.setUpIfNecessary(); setContent(mapMapVaultController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onModsSelected(ActionEvent event) { modVaultController.setUpIfNecessary(); setContent(modVaultController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onReplaysSelected(ActionEvent event) { // FIXME don't load every time? replayVaultController.loadLocalReplaysInBackground(); replayVaultController.loadOnlineReplaysInBackground(); setContent(replayVaultController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onLeaderboardRanked1v1Selected(ActionEvent event) { leaderboardController.setUpIfNecessary(); setContent(leaderboardController.getRoot()); setActiveNavigationButtonFromChild((MenuItem) event.getTarget()); } @FXML void onAeonButtonClicked() { gameService.startSearchRanked1v1(Faction.AEON); } @FXML void onUefButtonClicked() { gameService.startSearchRanked1v1(Faction.UEF); } @FXML void onCybranButtonClicked() { gameService.startSearchRanked1v1(Faction.CYBRAN); } @FXML void onSeraphimButtonClicked() { gameService.startSearchRanked1v1(Faction.SERAPHIM); } public void onUsernameButtonClicked(ActionEvent event) { Button button = (Button) event.getSource(); Bounds screenBounds = button.localToScreen(button.getBoundsInLocal()); userMenuPopup.show(button.getScene().getWindow(), screenBounds.getMaxX(), screenBounds.getMaxY()); } public void selectChatTab() { chatButton.fire(); } }