Feat: Game UI optimization #342
@@ -1,7 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -11,6 +13,9 @@ public class GameService {
|
||||
|
||||
private final GameClient client;
|
||||
private GameState state;
|
||||
private final Queue<Runnable> actionQueue = new LinkedList<>();
|
||||
private volatile boolean actionInProgress = false;
|
||||
private static final long ACTION_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* Constructs a GameService with the specified GameClient.
|
||||
@@ -112,12 +117,12 @@ public class GameService {
|
||||
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void call() {
|
||||
client.sendCall();
|
||||
queueAction(() -> client.sendCall());
|
||||
}
|
||||
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void fold() {
|
||||
client.sendFold();
|
||||
queueAction(() -> client.sendFold());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +131,7 @@ public class GameService {
|
||||
* @param amount The amount to bet. Must be a positive integer.
|
||||
*/
|
||||
public void bet(int amount) {
|
||||
client.sendBet(amount);
|
||||
queueAction(() -> client.sendBet(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +140,63 @@ public class GameService {
|
||||
* @param amount The amount to raise. Must be a positive integer.
|
||||
*/
|
||||
public void raise(int amount) {
|
||||
client.sendRaise(amount);
|
||||
queueAction(() -> client.sendRaise(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action to be executed sequentially. Only one action is processed at a time to
|
||||
* prevent concurrent operations from overwhelming the server or UI.
|
||||
*
|
||||
* @param action The action to queue and execute.
|
||||
*/
|
||||
private void queueAction(Runnable action) {
|
||||
synchronized (actionQueue) {
|
||||
actionQueue.offer(action);
|
||||
if (!actionInProgress) {
|
||||
processNextAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next queued action if one is available. This method ensures that only one action
|
||||
* is in progress at any given time.
|
||||
*/
|
||||
private void processNextAction() {
|
||||
synchronized (actionQueue) {
|
||||
if (actionQueue.isEmpty() || actionInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInProgress = true;
|
||||
Runnable action = actionQueue.poll();
|
||||
|
||||
if (action != null) {
|
||||
try {
|
||||
LOG.fine("Processing queued action");
|
||||
action.run();
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Error executing queued action: " + e.getMessage(), e);
|
||||
} finally {
|
||||
actionInProgress = false;
|
||||
if (!actionQueue.isEmpty()) {
|
||||
processNextAction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action is currently in progress, preventing the UI from accepting new actions
|
||||
* while one is being processed.
|
||||
*
|
||||
* @return true if an action is in progress, false otherwise.
|
||||
*/
|
||||
public boolean isActionInProgress() {
|
||||
return actionInProgress;
|
||||
}
|
||||
|
||||
/** Ensures that the game state has been initialized before accessing it. */
|
||||
|
||||
@@ -92,19 +92,39 @@ public class GameClient {
|
||||
}
|
||||
|
||||
public void sendCall() {
|
||||
client.processCommand("CALL GAME_ID=" + gameId);
|
||||
try {
|
||||
client.processCommand("CALL GAME_ID=" + gameId);
|
||||
LOG.fine("CALL command sent");
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send CALL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendFold() {
|
||||
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||
try {
|
||||
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||
LOG.fine("FOLD command sent");
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send FOLD: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendBet(int amount) {
|
||||
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
try {
|
||||
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
LOG.fine("BET command sent: amount=" + amount);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send BET: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendRaise(int amount) {
|
||||
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
try {
|
||||
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
LOG.fine("RAISE command sent: amount=" + amount);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send RAISE: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private GameState parseGameState(String input) {
|
||||
|
||||
+74
-14
@@ -23,6 +23,9 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -33,6 +36,7 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* Controller for the casino gaming area.
|
||||
@@ -122,6 +126,8 @@ public class CasinoGameController {
|
||||
private static final int DEALER_PLAYER_3 = 2;
|
||||
private static final int DEALER_MYSELF = 3;
|
||||
private static final double CARD_START_OPACITY = 0.0;
|
||||
private static final int TARGET_FPS = 30;
|
||||
private static final double FRAME_TIME_MS = 1000.0 / TARGET_FPS;
|
||||
private static final double CARD_START_SCALE = 0.85;
|
||||
private static final double COMMUNITY_CARD_HEIGHT_RATIO = 0.22;
|
||||
private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11;
|
||||
@@ -149,16 +155,17 @@ public class CasinoGameController {
|
||||
private static final double CHIP_TRANSLATE_RESET_Y = 0.0;
|
||||
private static final double CHIP_FADE_TO = 1.0;
|
||||
private static final double CHIP_SCALE_TO = 1.0;
|
||||
private static final long CHIP_FADE_DURATION_MS = 200;
|
||||
private static final long CHIP_SCALE_DURATION_MS = 220;
|
||||
private static final long CHIP_DROP_DURATION_MS = 250;
|
||||
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
|
||||
private static final long CHIP_FADE_DURATION_MS = 100;
|
||||
private static final long CHIP_SCALE_DURATION_MS = 110;
|
||||
private static final long CHIP_DROP_DURATION_MS = 120;
|
||||
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 15L;
|
||||
private static final int MAX_POT_ROWS = 2;
|
||||
private static final double POT_CHIP_H_GAP = 6.0;
|
||||
private static final double POT_CHIP_V_GAP = 6.0;
|
||||
private static final double POT_WIDTH_FACTOR = 0.45;
|
||||
private static final double FALLBACK_POT_WIDTH = 500.0;
|
||||
private static final double FALLBACK_CHIP_WIDTH = 36.0;
|
||||
private volatile boolean updating = false;
|
||||
private static final double CHAT_WIDTH = 400;
|
||||
private static final double CHAT_HEIGHT = 600;
|
||||
private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn";
|
||||
@@ -178,6 +185,7 @@ public class CasinoGameController {
|
||||
private static final int NO_BET = 0;
|
||||
private boolean myDealerIconSizeBound;
|
||||
private java.util.List<String> lobbyPlayerNames = java.util.List.of();
|
||||
private static final int MAX_ANIMATED_CHIPS = 12;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public CasinoGameController() {
|
||||
@@ -498,12 +506,24 @@ public class CasinoGameController {
|
||||
private void startLoop() {
|
||||
|
||||
timeline =
|
||||
new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(
|
||||
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
|
||||
e -> updateUI()));
|
||||
new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.millis(FRAME_TIME_MS),
|
||||
e -> {
|
||||
if (updating) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeline.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||
updating = true;
|
||||
|
||||
try {
|
||||
updateUI();
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}));
|
||||
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
@@ -625,6 +645,12 @@ public class CasinoGameController {
|
||||
updateGameInfo(s);
|
||||
highlightDealer(s);
|
||||
updateTaskbar(s);
|
||||
|
||||
if (s.pot != lastPot) {
|
||||
lastPot = s.pot;
|
||||
renderPot(s.pot);
|
||||
}
|
||||
|
||||
clearActiveTurnHighlights();
|
||||
announceLocalWinIfNeeded(s);
|
||||
finishGameUiLoop();
|
||||
@@ -644,6 +670,9 @@ public class CasinoGameController {
|
||||
highlightDealer(s);
|
||||
updateTaskbar(s);
|
||||
|
||||
// Force refresh of money display to handle UI stalls
|
||||
refreshMoneyDisplay();
|
||||
|
||||
LOGGER.info("myPlayerId=" + myPlayerId);
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
Player p = players.get(i);
|
||||
@@ -903,6 +932,30 @@ public class CasinoGameController {
|
||||
controller.update(s, myPlayerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh of the money display in the taskbar to ensure it always reflects the current
|
||||
* player's chip count, even during rapid game state changes.
|
||||
*/
|
||||
private void refreshMoneyDisplay() {
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller == null || gameService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<Player> players = gameService.getPlayers();
|
||||
if (players != null) {
|
||||
for (Player p : players) {
|
||||
if (isCurrentPlayer(p)) {
|
||||
controller.setMoney(p.getChips());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warning("Could not refresh money display: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the opponent player slots based on the list of players in the game state. This method
|
||||
* identifies the opponents by comparing their PlayerIds with myPlayerId and updates the
|
||||
@@ -1829,6 +1882,10 @@ public class CasinoGameController {
|
||||
* @param pot The total amount in the pot that needs to be represented with chips on the UI.
|
||||
*/
|
||||
private void renderPot(int pot) {
|
||||
if (!javafx.application.Platform.isFxApplicationThread()) {
|
||||
javafx.application.Platform.runLater(() -> renderPot(pot));
|
||||
return;
|
||||
}
|
||||
|
||||
potBox.getChildren().clear();
|
||||
|
||||
@@ -1869,7 +1926,13 @@ public class CasinoGameController {
|
||||
bottomRow.getChildren().add(chip);
|
||||
}
|
||||
|
||||
animateChipAppear(chip, index);
|
||||
if (index < MAX_ANIMATED_CHIPS) {
|
||||
animateChipAppear(chip, index);
|
||||
} else {
|
||||
chip.setOpacity(CHIP_FADE_TO);
|
||||
chip.setScaleX(CHIP_SCALE_TO);
|
||||
chip.setScaleY(CHIP_SCALE_TO);
|
||||
}
|
||||
|
||||
remaining -= chipValue;
|
||||
index++;
|
||||
@@ -1961,10 +2024,7 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void bindChipSize(ImageView view, javafx.scene.Scene scene) {
|
||||
|
||||
view.fitHeightProperty()
|
||||
.bind(
|
||||
scene.heightProperty().multiply(CHIP_HEIGHT_RATIO) // kleiner als Karten
|
||||
);
|
||||
view.fitHeightProperty().bind(scene.heightProperty().multiply(CHIP_HEIGHT_RATIO));
|
||||
|
||||
view.fitWidthProperty().bind(scene.widthProperty().multiply(CHIP_WIDTH_RATIO));
|
||||
}
|
||||
|
||||
+13
-10
@@ -112,6 +112,7 @@ public class CasinoBrowserController {
|
||||
TRUSTED_DOMAINS.add("mojeek.com");
|
||||
TRUSTED_DOMAINS.add("searx.be");
|
||||
TRUSTED_DOMAINS.add("startpage.com");
|
||||
TRUSTED_DOMAINS.add("vscode.dev");
|
||||
}
|
||||
|
||||
/** Aliases for common websites. */
|
||||
@@ -138,7 +139,10 @@ public class CasinoBrowserController {
|
||||
Map.entry("gradle", "gradle.org"),
|
||||
Map.entry("spring", "spring.io"),
|
||||
Map.entry("jetbrains", "jetbrains.com"),
|
||||
Map.entry("uni", "unibas.ch"));
|
||||
Map.entry("uni", "unibas.ch"),
|
||||
Map.entry("vs", "vscode.dev"),
|
||||
Map.entry("vscode", "vscode.dev"),
|
||||
Map.entry("vs code", "vscode.dev"));
|
||||
|
||||
/**
|
||||
* Resolves user input into a valid URL.
|
||||
@@ -653,23 +657,22 @@ public class CasinoBrowserController {
|
||||
}
|
||||
|
||||
if ("file".equalsIgnoreCase(scheme)) {
|
||||
Path allowed =
|
||||
Paths.get(
|
||||
System.getProperty("user.dir"),
|
||||
"documents",
|
||||
"docs",
|
||||
"game-engine",
|
||||
"manual.html")
|
||||
.normalize();
|
||||
|
||||
Path baseDir = Paths.get(System.getProperty("user.dir")).normalize();
|
||||
|
||||
Path manual = baseDir.resolve("documents/docs/game-engine/manual.html").normalize();
|
||||
|
||||
Path outreach = baseDir.resolve("outreach/index.html").normalize();
|
||||
|
||||
Path requested = Paths.get(uri).normalize();
|
||||
|
||||
if (requested.equals(allowed)) {
|
||||
if (requested.equals(manual) || requested.equals(outreach)) {
|
||||
securityLabel.setText("LOCAL OK");
|
||||
engine.load(uri.toString());
|
||||
} else {
|
||||
securityLabel.setText("BLOCKED");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+119
-18
@@ -91,6 +91,7 @@ public class TaskbarController {
|
||||
private int lastPotSnapshot = 0;
|
||||
private int lastObservedIncrease = 0;
|
||||
private int lastRaiseIncrement = 0;
|
||||
private static final long REFRESH_DELAY_MS = 500;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public TaskbarController() {
|
||||
@@ -568,7 +569,17 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstFlopPlayer(state, me)) {
|
||||
// if (isFirstFlopPlayer(state, me)) {
|
||||
// setActionEnabled(betButton, true);
|
||||
// setActionEnabled(callButton, false);
|
||||
// // setActionEnabled(foldButton, false);
|
||||
// setActionEnabled(raiseButton, false);
|
||||
// activateInputField(taskbarInput);
|
||||
// inputActionAllowed = true;
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (isFirstPlayerOfPhase(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
// setActionEnabled(foldButton, false);
|
||||
@@ -754,6 +765,35 @@ public class TaskbarController {
|
||||
return isInitialPreflopBlindLayout(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first player who can still act when starting from a specific index.
|
||||
*
|
||||
* @param state The current game state.
|
||||
* @param startIndex The index from which the search should begin.
|
||||
* @return The first eligible player index, or -1 if none can act.
|
||||
*/
|
||||
private int findFirstActingPlayerIndex(GameState state, int startIndex) {
|
||||
if (state == null || state.players == null || state.players.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int size = state.players.size();
|
||||
int normalizedStart = ((startIndex % size) + size) % size;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
int candidate = (normalizedStart + i) % size;
|
||||
Player player = state.players.get(candidate);
|
||||
|
||||
if (player != null
|
||||
&& player.getState() != PlayerState.FOLDED
|
||||
&& player.getChips() > 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current player is the first to act on the flop.
|
||||
*
|
||||
@@ -783,19 +823,10 @@ public class TaskbarController {
|
||||
return false;
|
||||
}
|
||||
|
||||
int firstIndex = (state.dealer + 1) % size;
|
||||
int firstIndex = findFirstActingPlayerIndex(state, (state.dealer + 1) % size);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
|
||||
int candidate = (firstIndex + i) % size;
|
||||
|
||||
Player p = state.players.get(candidate);
|
||||
|
||||
if (p != null && p.getState() != PlayerState.FOLDED && p.getChips() > 0) {
|
||||
|
||||
firstIndex = candidate;
|
||||
break;
|
||||
}
|
||||
if (firstIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state.activePlayer == firstIndex && myIndex == firstIndex;
|
||||
@@ -828,14 +859,19 @@ public class TaskbarController {
|
||||
return false;
|
||||
}
|
||||
|
||||
int dealer = state.dealer;
|
||||
|
||||
int firstIndex;
|
||||
|
||||
if (isPreflop(state.phase)) {
|
||||
firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
|
||||
firstIndex =
|
||||
findFirstActingPlayerIndex(
|
||||
state,
|
||||
(size == 2) ? state.dealer : (state.dealer + DEALER_OFFSET) % size);
|
||||
} else {
|
||||
firstIndex = (dealer + 1) % size;
|
||||
firstIndex = findFirstActingPlayerIndex(state, (state.dealer + 1) % size);
|
||||
}
|
||||
|
||||
if (firstIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state.activePlayer == firstIndex && myIndex == firstIndex;
|
||||
@@ -989,6 +1025,11 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameService.isActionInProgress()) {
|
||||
LOGGER.info("Action {} blocked: another action is in progress", actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state = ensureLatestStateForAction(actionType.name().toLowerCase());
|
||||
if (state == null || isHandFinished(state)) {
|
||||
if (state != null) {
|
||||
@@ -1014,6 +1055,8 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
disableAllActionButtons();
|
||||
|
||||
executeAction(state, targetBet, actionType);
|
||||
|
||||
if (targetBet > 0) {
|
||||
@@ -1021,7 +1064,35 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
taskbarInput.clear();
|
||||
refreshGame();
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(REFRESH_DELAY_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
refreshGame();
|
||||
});
|
||||
}
|
||||
|
||||
/** Disables all action buttons to prevent multiple concurrent action submissions. */
|
||||
private void disableAllActionButtons() {
|
||||
if (betButton != null) {
|
||||
betButton.setDisable(true);
|
||||
}
|
||||
if (callButton != null) {
|
||||
callButton.setDisable(true);
|
||||
}
|
||||
if (raiseButton != null) {
|
||||
raiseButton.setDisable(true);
|
||||
}
|
||||
if (foldButton != null) {
|
||||
foldButton.setDisable(true);
|
||||
}
|
||||
if (taskbarInput != null) {
|
||||
taskbarInput.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1791,6 +1862,24 @@ public class TaskbarController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the integrated Casono web browser.
|
||||
*
|
||||
* <p>once the content for strategies and support is available.
|
||||
*/
|
||||
@FXML
|
||||
private void onBrowserButtonClickCasono() {
|
||||
SoundManager.getInstance().playButtonClick();
|
||||
try {
|
||||
Path path = Paths.get(System.getProperty("user.dir"), "outreach", "index.html");
|
||||
|
||||
CasinoBrowserController.open(path.toUri().toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the integrated Casono Web Browser. */
|
||||
@FXML
|
||||
private void onBrowserButtonClickWiki() {
|
||||
@@ -1815,6 +1904,18 @@ public class TaskbarController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the integrated Casono Web Browser. */
|
||||
@FXML
|
||||
private void onBrowserButtonClickVSCode() {
|
||||
try {
|
||||
|
||||
CasinoBrowserController.open("https://vscode.dev/");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the tips notebook if it's currently closed. */
|
||||
@FXML
|
||||
private void onShowTipsButtonClick() {
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
<MenuButton text="HELP" styleClass="gray-button">
|
||||
<items>
|
||||
<MenuItem text="SHOW TIPS" onAction="#onShowTipsButtonClick" />
|
||||
<MenuItem text="CASONO BROWSER: Manual" onAction="#onBrowserButtonClick" />
|
||||
<MenuItem text="CASONO BROWSER: Wikipedia" onAction="#onBrowserButtonClickWiki" />
|
||||
<MenuItem text="CASONO BROWSER: Brave Search" onAction="#onBrowserButtonClickBrave" />
|
||||
<MenuItem text="CB: Manual" onAction="#onBrowserButtonClick" />
|
||||
<MenuItem text="CB: Casono" onAction="#onBrowserButtonClickCasono" />
|
||||
<MenuItem text="CB: Wikipedia" onAction="#onBrowserButtonClickWiki" />
|
||||
<MenuItem text="CB: Brave Search" onAction="#onBrowserButtonClickBrave" />
|
||||
<MenuItem text="CB: VS Code" onAction="#onBrowserButtonClickVSCode" />
|
||||
</items>
|
||||
</MenuButton>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user