package market.guess.ui.desktop.controllers; import java.util.ArrayList; import java.util.function.Consumer; import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.LineChart; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.ProgressBar; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import market.guess.model.event.CommissionTiming; import market.guess.model.event.EventStatus; import market.guess.model.event.MechanismType; import market.guess.ui.desktop.AppState; import market.guess.ui.desktop.components.CommissionFilter; import market.guess.ui.desktop.components.EventListCell; import market.guess.ui.desktop.components.LadderItemView; import market.guess.ui.desktop.components.MechanismFilter; import market.guess.ui.desktop.components.ParticipantItemView; import market.guess.ui.desktop.components.TradeHistoryItemView; import market.guess.ui.desktop.components.graphic.EqualizerGraphic; import market.guess.ui.desktop.components.graphic.Graphic; import market.guess.ui.desktop.model.ChartPoint; import market.guess.ui.desktop.model.EventData; import market.guess.ui.desktop.model.OrderBook; import market.guess.ui.desktop.model.ParticipantRow; import market.guess.ui.desktop.model.TradeRow; import market.guess.ui.desktop.util.Charts; import market.guess.ui.desktop.util.Format; import market.guess.ui.desktop.util.Views; public class EventsTabController { private static final PseudoClass ACTIVE = PseudoClass.getPseudoClass("active"); private static final PseudoClass CLOSED = PseudoClass.getPseudoClass("closed"); private static final PseudoClass IDLE = PseudoClass.getPseudoClass("idle"); private AppState state; private Runnable onNewEvent; private Consumer onOpenEvent; private Runnable onCloseEvent; private Graphic equalizer; private boolean updatingSelection = false; @FXML private Button methodFilterAllBtn; @FXML private Button methodFilterLmsrBtn; @FXML private Button methodFilterObBtn; @FXML private Button statusFilterAllBtn; @FXML private Button statusFilterNotStartedBtn; @FXML private Button statusFilterActiveBtn; @FXML private Button statusFilterClosedBtn; @FXML private Button feeFilterAllBtn; @FXML private Button feeFilterPurchaseBtn; @FXML private Button feeFilterCloseBtn; @FXML private ListView eventListContainer; @FXML private Label eventCountLabel; @FXML private VBox eventDetailCard; @FXML private Label eventNameLabel; @FXML private Label eventDescLabel; @FXML private Button openEventBtn; @FXML private Button closeEventBtn; @FXML private Label metaMethodLabel; @FXML private Label metaStatusLabel; @FXML private HBox livePill; @FXML private StackPane liveBarsBox; @FXML private Label liveLabel; @FXML private Label metaMmLabel; @FXML private Label metaFeeLabel; @FXML private Label metaContractLabel; @FXML private VBox lmsrSection; @FXML private Label lmsrYesPriceLabel; @FXML private ProgressBar lmsrYesProgressBar; @FXML private Label lmsrYesSharesLabel; @FXML private Label lmsrNoPriceLabel; @FXML private ProgressBar lmsrNoProgressBar; @FXML private Label lmsrNoSharesLabel; @FXML private Label lmsrChartSubLabel; @FXML private LineChart lmsrChart; @FXML private VBox tradeHistoryRowsContainer; @FXML private VBox orderBookSection; @FXML private Label yesBookCountLabel; @FXML private Label yesLastLabel; @FXML private Label yesMidLabel; @FXML private Label yesBestBidLabel; @FXML private Label yesBestAskLabel; @FXML private Label yesSpreadLabel; @FXML private VBox yesBookLadderList; @FXML private Label noBookCountLabel; @FXML private Label noLastLabel; @FXML private Label noMidLabel; @FXML private Label noBestBidLabel; @FXML private Label noBestAskLabel; @FXML private Label noSpreadLabel; @FXML private VBox noBookLadderList; @FXML private Label obChartSubLabel; @FXML private LineChart obChart; @FXML private VBox participantsRowsContainer; @FXML private HBox resolvedBanner; @FXML private Label resolvedBannerText; public void init(AppState state) { this.state = state; equalizer = EqualizerGraphic.create(); liveBarsBox.getChildren().setAll(equalizer.getNode()); eventListContainer.setCellFactory(lv -> new EventListCell()); eventListContainer .getSelectionModel() .selectedItemProperty() .addListener( (obs, oldVal, newVal) -> { if (updatingSelection) return; if (newVal != null && state != null) { state.setSelectedEvent(newVal); } }); state .selectedEventProperty() .addListener( (obs, oldVal, newVal) -> { if (newVal != null) { if (!newVal.equals(eventListContainer.getSelectionModel().getSelectedItem())) { updatingSelection = true; try { eventListContainer.getSelectionModel().select(newVal); } finally { updatingSelection = false; } } } refreshDetailCard(newVal); }); state.filterMethodProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); state.filterStatusProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); state.filterFeeProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); state .getEvents() .addListener( (ListChangeListener) c -> { refreshListAndFilters(); refreshDetailCard(state.selectedEvent()); }); state .animationsOnProperty() .addListener( (obs, oldVal, newVal) -> { var sel = state.selectedEvent(); boolean isActive = sel != null && sel.status == EventStatus.ACTIVE; equalizer.setPlaying(newVal && isActive); }); } public void setOnNewEvent(Runnable onNewEvent) { this.onNewEvent = onNewEvent; } public void setOnOpenEvent(Consumer onOpenEvent) { this.onOpenEvent = onOpenEvent; } public void setOnCloseEvent(Runnable onCloseEvent) { this.onCloseEvent = onCloseEvent; } public void refresh() { if (state == null) return; refreshListAndFilters(); refreshDetailCard(state.selectedEvent()); } private void refreshListAndFilters() { if (state == null) return; // 1. Update filter button selections Views.setSelected(methodFilterAllBtn, state.getFilterMethod() == MechanismFilter.ALL); Views.setSelected(methodFilterLmsrBtn, state.getFilterMethod() == MechanismFilter.LMSR); Views.setSelected(methodFilterObBtn, state.getFilterMethod() == MechanismFilter.ORDER_BOOK); Views.setSelected(statusFilterAllBtn, state.getFilterStatus() == null); Views.setSelected(statusFilterNotStartedBtn, state.getFilterStatus() == EventStatus.DRAFT); Views.setSelected(statusFilterActiveBtn, state.getFilterStatus() == EventStatus.ACTIVE); Views.setSelected(statusFilterClosedBtn, state.getFilterStatus() == EventStatus.SETTLED); Views.setSelected(feeFilterAllBtn, state.getFilterFee() == CommissionFilter.ALL); Views.setSelected(feeFilterPurchaseBtn, state.getFilterFee() == CommissionFilter.PURCHASE); Views.setSelected(feeFilterCloseBtn, state.getFilterFee() == CommissionFilter.CLOSE); // 2. Update list var filtered = state.getEvents().stream() .filter( e -> state.getFilterMethod() == MechanismFilter.ALL || (state.getFilterMethod() == MechanismFilter.LMSR && e.type == MechanismType.LMSR) || (state.getFilterMethod() == MechanismFilter.ORDER_BOOK && e.type == MechanismType.ORDER_BOOK)) .filter(e -> state.getFilterStatus() == null || e.status == state.getFilterStatus()) .filter( e -> state.getFilterFee() == CommissionFilter.ALL || (state.getFilterFee() == CommissionFilter.PURCHASE && e.feeMode == CommissionTiming.ON_PURCHASE) || (state.getFilterFee() == CommissionFilter.CLOSE && e.feeMode == CommissionTiming.ON_CLOSE)) .toList(); updatingSelection = true; try { eventListContainer.getItems().setAll(filtered); eventCountLabel.setText( filtered.size() + " of " + state.getEvents().size() + " events shown"); var sel = state.selectedEvent(); EventData match = null; if (sel != null) { for (var e : filtered) { if (e.id.equals(sel.id)) { match = e; break; } } } if (match != null) { eventListContainer.getSelectionModel().select(match); } else if (!filtered.isEmpty()) { eventListContainer.getSelectionModel().select(0); } } finally { updatingSelection = false; } } private void refreshDetailCard(EventData selected) { if (selected == null) { eventDetailCard.setVisible(false); return; } eventDetailCard.setVisible(true); eventNameLabel.setText(selected.num + ". " + selected.name); eventDescLabel.setText(selected.desc); // Open = go trade: any unblocked user on an ACTIVE event, or the MM activating their DRAFT. var actor = state.getActingUser(); boolean isMm = selected.mm.equals(state.getActingUserName()); boolean canOpen = actor != null && !actor.blocked && (selected.status == EventStatus.ACTIVE || (selected.status == EventStatus.DRAFT && isMm)); boolean canClose = selected.status == EventStatus.ACTIVE && selected.mm.equals(state.getActingUserName()); openEventBtn.setText(selected.status == EventStatus.DRAFT ? "Start Event" : "Trade"); openEventBtn.setVisible(canOpen); openEventBtn.setManaged(canOpen); closeEventBtn.setVisible(canClose); closeEventBtn.setManaged(canClose); // Meta fields metaMethodLabel.setText(selected.type == MechanismType.LMSR ? "LMSR" : "Order Book"); metaStatusLabel.setText(selected.status.name()); // LIVE pill: only an ACTIVE market needs to look like order flow is arriving. boolean isActive = selected.status == EventStatus.ACTIVE; boolean isEnded = selected.status == EventStatus.SETTLED; liveLabel.setText(isActive ? "LIVE" : isEnded ? "ENDED" : "IDLE"); livePill.pseudoClassStateChanged(ACTIVE, isActive); livePill.pseudoClassStateChanged(CLOSED, isEnded); livePill.pseudoClassStateChanged(IDLE, !isActive && !isEnded); equalizer.setPlaying(state.isAnimationsOn() && isActive); metaMmLabel.setText(selected.mm); metaFeeLabel.setText(selected.feeText()); metaContractLabel.setText(Format.money(selected.contract)); // Sections boolean isLmsr = selected.type == MechanismType.LMSR; lmsrSection.setVisible(isLmsr); lmsrSection.setManaged(isLmsr); orderBookSection.setVisible(!isLmsr); orderBookSection.setManaged(!isLmsr); if (isLmsr) { refreshLmsr(selected); } else { refreshOrderBook(selected); } // Trade history table var tRows = new ArrayList(selected.trades.size()); for (var t : selected.trades) { tRows.add(buildTradeHistoryRow(t)); } tradeHistoryRowsContainer.getChildren().setAll(tRows); // Participants table var pRows = new ArrayList(selected.participants.size()); for (var p : selected.participants) { pRows.add(buildParticipantRow(p)); } participantsRowsContainer.getChildren().setAll(pRows); // Resolved banner boolean isClosed = selected.status == EventStatus.SETTLED; resolvedBanner.setVisible(isClosed); resolvedBanner.setManaged(isClosed); if (isClosed) { resolvedBannerText.setText( "Winning option: " + selected.resolvedOption + "  " + selected.yesShares + " YES / " + selected.noShares + " NO shares bought  contract emptied to holders, " + selected.feePercent + "% fee moved to " + selected.mm + "."); } } private void refreshLmsr(EventData e) { lmsrYesPriceLabel.setText(Format.money(e.yesPrice)); lmsrYesProgressBar.setProgress(e.yesPrice.doubleValue()); lmsrYesSharesLabel.setText( e.yesShares + " shares  p = " + Format.prob(e.yesPrice.doubleValue())); lmsrNoPriceLabel.setText(Format.money(e.noPrice)); lmsrNoProgressBar.setProgress(e.noPrice.doubleValue()); lmsrNoSharesLabel.setText(e.noShares + " shares  p = " + Format.prob(e.noPrice.doubleValue())); lmsrChartSubLabel.setText("YES / NO  LMSR b = " + e.liquidityB); var noPts = new ArrayList(); for (var p : e.chart) noPts.add(new ChartPoint(p.x(), 1.0 - p.y())); Charts.setDualSeries(lmsrChart, "YES", e.chart, "NO", noPts); } private void refreshOrderBook(EventData e) { obChartSubLabel.setText( "last trade YES  base d = " + (e.baseValueD != null ? Format.money(e.baseValueD) : "—")); Charts.setSingleSeries(obChart, "value", e.chart); populateBookSide( e.yesBook, yesBookCountLabel, yesLastLabel, yesBestBidLabel, yesBestAskLabel, yesMidLabel, yesSpreadLabel, yesBookLadderList); populateBookSide( e.noBook, noBookCountLabel, noLastLabel, noBestBidLabel, noBestAskLabel, noMidLabel, noSpreadLabel, noBookLadderList); } private void populateBookSide( OrderBook book, Label countLabel, Label lastLabel, Label bestBidLabel, Label bestAskLabel, Label midLabel, Label spreadLabel, VBox ladderList) { if (book == null) { ladderList.getChildren().clear(); countLabel.setText("0 orders"); for (var l : new Label[] {lastLabel, bestBidLabel, bestAskLabel, midLabel, spreadLabel}) { l.setText("—"); } return; } countLabel.setText(book.rows.size() + " orders"); lastLabel.setText(book.last != null ? book.last : "—"); bestBidLabel.setText(book.bid != null ? book.bid : "—"); bestAskLabel.setText(book.ask != null ? book.ask : "—"); midLabel.setText(book.mid != null ? book.mid : "—"); spreadLabel.setText(book.spread != null ? book.spread : "—"); var rows = new ArrayList(book.rows.size()); for (var r : book.rows) { rows.add(new LadderItemView(r)); } ladderList.getChildren().setAll(rows); } private Node buildTradeHistoryRow(TradeRow t) { return new TradeHistoryItemView(t); } private Node buildParticipantRow(ParticipantRow p) { return new ParticipantItemView(p); } // ---- FXML Handlers ---------------------------------------------------- @FXML private void handleMethodAll() { if (state != null) state.setFilterMethod(MechanismFilter.ALL); } @FXML private void handleMethodLmsr() { if (state != null) state.setFilterMethod(MechanismFilter.LMSR); } @FXML private void handleMethodOb() { if (state != null) state.setFilterMethod(MechanismFilter.ORDER_BOOK); } @FXML private void handleStatusAll() { if (state != null) state.setFilterStatus(null); } @FXML private void handleStatusNotStarted() { if (state != null) state.setFilterStatus(EventStatus.DRAFT); } @FXML private void handleStatusActive() { if (state != null) state.setFilterStatus(EventStatus.ACTIVE); } @FXML private void handleStatusClosed() { if (state != null) state.setFilterStatus(EventStatus.SETTLED); } @FXML private void handleFeeAll() { if (state != null) state.setFilterFee(CommissionFilter.ALL); } @FXML private void handleFeePurchase() { if (state != null) state.setFilterFee(CommissionFilter.PURCHASE); } @FXML private void handleFeeClose() { if (state != null) state.setFilterFee(CommissionFilter.CLOSE); } @FXML private void handleNewEvent() { if (onNewEvent != null) { onNewEvent.run(); } } @FXML private void handleOpenEvent() { if (state != null) { var e = state.selectedEvent(); if (e != null && onOpenEvent != null) { onOpenEvent.accept(e); } } } @FXML private void handleCloseEvent() { if (onCloseEvent != null) { onCloseEvent.run(); } } }