diff options
| author | Kostya <mail@sartin.in> | 2026-09-09 10:05:12 +0300 |
|---|---|---|
| committer | Kostya <mail@sartin.in> | 2026-09-09 10:05:12 +0300 |
| commit | f23c20c1c66b39fb1f49307e08e8593fa708e026 (patch) | |
| tree | 35cf625f5bbb1acf079b6abd2e239fcd446cd2a5 /ui-desktop/src | |
| parent | 8d7cecd9cf9fb2b0b45607b8efd7ca8f19a89fc6 (diff) | |
| download | guess-market-f23c20c1c66b39fb1f49307e08e8593fa708e026.tar.gz guess-market-f23c20c1c66b39fb1f49307e08e8593fa708e026.tar.xz guess-market-f23c20c1c66b39fb1f49307e08e8593fa708e026.zip | |
ui-desktop: build the JavaFX market UI on top of the service
Replace the placeholder hello-world window with a working desktop
client for the guess market.
ServiceEngine wires the in-memory repositories, XML loader, risk,
matching, fulfillment and settlement contexts together, and AppState
exposes their results as observable JavaFX state for the views to bind
to. The FXML views have an events tab and a users tab. The events tab
lists events with mechanism and commission filters, and shows each
event's price chart, order book ladder, trade history and participants.
The users tab shows each user's ledger and positions, and lets them
place buy and sell orders. Dialogs cover loading an XML file (in a
background task), creating an event and resolving one.
Bundle a CSS theme and Nerd Font faces so the UI looks the same on
every machine. Teach build.sh to copy each module's src/main/resources
into its class output, since FXML, CSS and fonts are loaded from the
classpath at runtime.
Diffstat (limited to 'ui-desktop/src')
65 files changed, 6426 insertions, 13 deletions
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/App.java b/ui-desktop/src/main/java/market/guess/ui/desktop/App.java index f0b2b7a..0d92c7d 100644 --- a/ui-desktop/src/main/java/market/guess/ui/desktop/App.java +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/App.java | |||
| @@ -1,22 +1,12 @@ | |||
| 1 | package market.guess.ui.desktop; | 1 | package market.guess.ui.desktop; |
| 2 | 2 | ||
| 3 | import javafx.application.Application; | 3 | import javafx.application.Application; |
| 4 | import javafx.scene.Scene; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.StackPane; | ||
| 7 | import javafx.stage.Stage; | 4 | import javafx.stage.Stage; |
| 8 | 5 | ||
| 9 | public class App extends Application { | 6 | public final class App extends Application { |
| 10 | @Override | 7 | @Override |
| 11 | public void start(Stage stage) throws Exception { | 8 | public void start(Stage stage) { |
| 12 | var javaVersion = System.getProperty("java.version"); | 9 | new AppView(stage); |
| 13 | var openjfxVersion = System.getProperty("javafx.version"); | ||
| 14 | |||
| 15 | var label = | ||
| 16 | new Label("Hello. JavaFX " + openjfxVersion + ", running on Java " + javaVersion + "."); | ||
| 17 | var scene = new Scene(new StackPane(label), 640, 480); | ||
| 18 | |||
| 19 | stage.setScene(scene); | ||
| 20 | stage.show(); | 10 | stage.show(); |
| 21 | } | 11 | } |
| 22 | } | 12 | } |
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java b/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java new file mode 100644 index 0000000..80e472b --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/AppState.java | |||
| @@ -0,0 +1,760 @@ | |||
| 1 | package market.guess.ui.desktop; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.time.Instant; | ||
| 5 | import java.time.LocalDateTime; | ||
| 6 | import java.time.ZoneId; | ||
| 7 | import java.time.format.DateTimeFormatter; | ||
| 8 | import java.util.ArrayList; | ||
| 9 | import java.util.Collections; | ||
| 10 | import java.util.List; | ||
| 11 | import javafx.beans.property.BooleanProperty; | ||
| 12 | import javafx.beans.property.ObjectProperty; | ||
| 13 | import javafx.beans.property.SimpleBooleanProperty; | ||
| 14 | import javafx.beans.property.SimpleObjectProperty; | ||
| 15 | import javafx.beans.property.SimpleStringProperty; | ||
| 16 | import javafx.beans.property.StringProperty; | ||
| 17 | import javafx.collections.FXCollections; | ||
| 18 | import javafx.collections.ListChangeListener; | ||
| 19 | import javafx.collections.ObservableList; | ||
| 20 | import market.guess.api.AccountContext; | ||
| 21 | import market.guess.api.CatalogContext; | ||
| 22 | import market.guess.api.GuessMarketContext; | ||
| 23 | import market.guess.model.event.EventDetailDTO; | ||
| 24 | import market.guess.model.event.EventStatus; | ||
| 25 | import market.guess.model.event.EventSummaryDTO; | ||
| 26 | import market.guess.model.event.MechanismType; | ||
| 27 | import market.guess.model.ledger.LedgerDTO; | ||
| 28 | import market.guess.ui.desktop.components.AppTab; | ||
| 29 | import market.guess.ui.desktop.components.CommissionFilter; | ||
| 30 | import market.guess.ui.desktop.components.MechanismFilter; | ||
| 31 | import market.guess.ui.desktop.components.ModalDialog; | ||
| 32 | import market.guess.ui.desktop.components.TradeSide; | ||
| 33 | import market.guess.ui.desktop.model.BookRow; | ||
| 34 | import market.guess.ui.desktop.model.ChartPoint; | ||
| 35 | import market.guess.ui.desktop.model.EventData; | ||
| 36 | import market.guess.ui.desktop.model.OrderBook; | ||
| 37 | import market.guess.ui.desktop.model.ParticipantRow; | ||
| 38 | import market.guess.ui.desktop.model.TradeRow; | ||
| 39 | import market.guess.ui.desktop.model.UserData; | ||
| 40 | import market.guess.ui.desktop.model.UserEventRow; | ||
| 41 | import market.guess.ui.desktop.util.Format; | ||
| 42 | |||
| 43 | /** Reactive UI state backed by JavaFX properties and observable collections. */ | ||
| 44 | public final class AppState { | ||
| 45 | private final CatalogContext catalogContext; | ||
| 46 | private final GuessMarketContext marketContext; | ||
| 47 | private final AccountContext accountContext; | ||
| 48 | |||
| 49 | private final StringProperty loadedFile = new SimpleStringProperty(null); | ||
| 50 | |||
| 51 | public final ObservableList<EventData> events = FXCollections.observableArrayList(); | ||
| 52 | public final ObservableList<UserData> users = FXCollections.observableArrayList(); | ||
| 53 | |||
| 54 | private final StringProperty selectedEventId = new SimpleStringProperty(null); | ||
| 55 | private final ObjectProperty<EventData> selectedEvent = new SimpleObjectProperty<>(); | ||
| 56 | |||
| 57 | private final StringProperty actingUserName = new SimpleStringProperty(null); | ||
| 58 | private final ObjectProperty<UserData> actingUser = new SimpleObjectProperty<>(); | ||
| 59 | |||
| 60 | private final StringProperty selectedUserEventId = new SimpleStringProperty("1"); | ||
| 61 | |||
| 62 | private final ObjectProperty<MechanismFilter> filterMethod = | ||
| 63 | new SimpleObjectProperty<>(MechanismFilter.ALL); | ||
| 64 | private final ObjectProperty<EventStatus> filterStatus = new SimpleObjectProperty<>(null); | ||
| 65 | private final ObjectProperty<CommissionFilter> filterFee = | ||
| 66 | new SimpleObjectProperty<>(CommissionFilter.ALL); | ||
| 67 | |||
| 68 | private final ObjectProperty<TradeSide> tradeSide = new SimpleObjectProperty<>(TradeSide.BUY); | ||
| 69 | private final StringProperty tradeQty = new SimpleStringProperty("50"); | ||
| 70 | private final StringProperty tradePrice = new SimpleStringProperty("0.62"); | ||
| 71 | private final BooleanProperty tradeOptionYes = new SimpleBooleanProperty(true); | ||
| 72 | |||
| 73 | private final ObjectProperty<Skin> skin = new SimpleObjectProperty<>(Skin.ROSE_PINE_DAWN); | ||
| 74 | private final BooleanProperty animationsOn = new SimpleBooleanProperty(true); | ||
| 75 | |||
| 76 | private final ObjectProperty<AppTab> activeTab = new SimpleObjectProperty<>(AppTab.EVENTS); | ||
| 77 | private final ObjectProperty<ModalDialog> dialog = new SimpleObjectProperty<>(ModalDialog.NONE); | ||
| 78 | |||
| 79 | private final ObjectProperty<MechanismType> createType = | ||
| 80 | new SimpleObjectProperty<>(MechanismType.LMSR); | ||
| 81 | private final BooleanProperty createMintingOn = new SimpleBooleanProperty(true); | ||
| 82 | private final StringProperty resolveSelectedOption = new SimpleStringProperty(null); | ||
| 83 | |||
| 84 | private final StringProperty toast = new SimpleStringProperty(null); | ||
| 85 | private final StringProperty pendingFile = new SimpleStringProperty(null); | ||
| 86 | |||
| 87 | public AppState() { | ||
| 88 | this(new ServiceEngine()); | ||
| 89 | } | ||
| 90 | |||
| 91 | public AppState(ServiceEngine engine) { | ||
| 92 | this(engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext()); | ||
| 93 | } | ||
| 94 | |||
| 95 | public AppState( | ||
| 96 | CatalogContext catalogContext, | ||
| 97 | GuessMarketContext marketContext, | ||
| 98 | AccountContext accountContext) { | ||
| 99 | this.catalogContext = catalogContext; | ||
| 100 | this.marketContext = marketContext; | ||
| 101 | this.accountContext = accountContext; | ||
| 102 | |||
| 103 | selectedEventId.addListener((obs, oldVal, newVal) -> updateSelectedEvent()); | ||
| 104 | events.addListener((ListChangeListener<EventData>) c -> updateSelectedEvent()); | ||
| 105 | |||
| 106 | actingUserName.addListener((obs, oldVal, newVal) -> updateActingUser()); | ||
| 107 | users.addListener((ListChangeListener<UserData>) c -> updateActingUser()); | ||
| 108 | |||
| 109 | refreshData(); | ||
| 110 | } | ||
| 111 | |||
| 112 | public void refreshData() { | ||
| 113 | var allEventsRes = catalogContext.getAllEvents(); | ||
| 114 | var allAccountsRes = accountContext.getAllAccounts(); | ||
| 115 | |||
| 116 | var newEventList = new ArrayList<EventData>(); | ||
| 117 | if (allEventsRes != null && allEventsRes.isSuccess() && allEventsRes.getData() != null) { | ||
| 118 | for (var summary : allEventsRes.getData()) { | ||
| 119 | var detailRes = catalogContext.getEvent(summary.key()); | ||
| 120 | EventDetailDTO detail = | ||
| 121 | (detailRes != null && detailRes.isSuccess()) ? detailRes.getData() : null; | ||
| 122 | newEventList.add(toEventData(summary, detail)); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | this.events.setAll(newEventList); | ||
| 126 | |||
| 127 | var newUserList = new ArrayList<UserData>(); | ||
| 128 | if (allAccountsRes != null && allAccountsRes.isSuccess() && allAccountsRes.getData() != null) { | ||
| 129 | for (var acc : allAccountsRes.getData()) { | ||
| 130 | newUserList.add(toUserData(acc, newEventList)); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | this.users.setAll(newUserList); | ||
| 134 | updateSelectedEvent(); | ||
| 135 | updateActingUser(); | ||
| 136 | } | ||
| 137 | |||
| 138 | private EventData toEventData(EventSummaryDTO summary, EventDetailDTO detail) { | ||
| 139 | double contractVal = 0.0; | ||
| 140 | try { | ||
| 141 | if (summary.accountBalance() != null) { | ||
| 142 | contractVal = | ||
| 143 | Double.parseDouble(summary.accountBalance().replace("$", "").replace(",", "").trim()); | ||
| 144 | } | ||
| 145 | } catch (Exception ignored) { | ||
| 146 | } | ||
| 147 | |||
| 148 | EventData ev = | ||
| 149 | new EventData( | ||
| 150 | summary.key(), | ||
| 151 | summary.displayId(), | ||
| 152 | summary.name(), | ||
| 153 | summary.description(), | ||
| 154 | summary.mechanism(), | ||
| 155 | summary.status(), | ||
| 156 | summary.marketMaker(), | ||
| 157 | summary.commissionPercent(), | ||
| 158 | summary.commissionTiming(), | ||
| 159 | contractVal, | ||
| 160 | summary.mechanism() == MechanismType.LMSR ? 100 : null, | ||
| 161 | summary.mechanism() == MechanismType.ORDER_BOOK ? BigDecimal.ONE : null); | ||
| 162 | |||
| 163 | if (detail != null) { | ||
| 164 | if (detail.state() != null && detail.state().markets() != null) { | ||
| 165 | var markets = detail.state().markets(); | ||
| 166 | if (markets.size() >= 1) { | ||
| 167 | try { | ||
| 168 | ev.yesPrice = new BigDecimal(markets.get(0).price()); | ||
| 169 | ev.yesShares = (int) Double.parseDouble(markets.get(0).volume()); | ||
| 170 | } catch (Exception ignored) { | ||
| 171 | } | ||
| 172 | } | ||
| 173 | if (markets.size() >= 2) { | ||
| 174 | try { | ||
| 175 | ev.noPrice = new BigDecimal(markets.get(1).price()); | ||
| 176 | ev.noShares = (int) Double.parseDouble(markets.get(1).volume()); | ||
| 177 | } catch (Exception ignored) { | ||
| 178 | } | ||
| 179 | } | ||
| 180 | } | ||
| 181 | if (detail.history() != null) { | ||
| 182 | int idx = 1; | ||
| 183 | for (var t : detail.history()) { | ||
| 184 | boolean isYes = | ||
| 185 | "YES".equalsIgnoreCase(t.optionName()) | ||
| 186 | || (summary.optionNames() != null | ||
| 187 | && !summary.optionNames().isEmpty() | ||
| 188 | && summary.optionNames().get(0).equalsIgnoreCase(t.optionName())); | ||
| 189 | int qty = 0; | ||
| 190 | try { | ||
| 191 | qty = (int) Long.parseLong(t.quantity()); | ||
| 192 | } catch (Exception ignored) { | ||
| 193 | } | ||
| 194 | ev.trades.add( | ||
| 195 | new TradeRow(idx++, t.userName(), t.optionName(), isYes, qty, "$" + t.pricePaid())); | ||
| 196 | } | ||
| 197 | } | ||
| 198 | ev.resolvedOption = detail.settledMarket(); | ||
| 199 | } | ||
| 200 | |||
| 201 | // Baseline and historical chart points | ||
| 202 | if (detail != null && detail.history() != null && !detail.history().isEmpty()) { | ||
| 203 | var historyChronological = new ArrayList<>(detail.history()); | ||
| 204 | Collections.reverse(historyChronological); | ||
| 205 | |||
| 206 | long firstT = parseEpochSecond(historyChronological.get(0).at()); | ||
| 207 | if (firstT == 0L) firstT = Instant.now().getEpochSecond(); | ||
| 208 | ev.chart.add(new ChartPoint(firstT - 60, 0.50)); | ||
| 209 | |||
| 210 | for (var t : historyChronological) { | ||
| 211 | long time = parseEpochSecond(t.at()); | ||
| 212 | if (time == 0L) time = firstT; | ||
| 213 | boolean isYes = | ||
| 214 | "YES".equalsIgnoreCase(t.optionName()) | ||
| 215 | || (summary.optionNames() != null | ||
| 216 | && !summary.optionNames().isEmpty() | ||
| 217 | && summary.optionNames().get(0).equalsIgnoreCase(t.optionName())); | ||
| 218 | double price = 0.50; | ||
| 219 | try { | ||
| 220 | double cost = Double.parseDouble(t.pricePaid().replace("$", "").replace(",", "").trim()); | ||
| 221 | long qty = Long.parseLong(t.quantity()); | ||
| 222 | if (qty > 0) { | ||
| 223 | double unitP = cost / qty; | ||
| 224 | price = isYes ? unitP : Math.max(0.0, 1.0 - unitP); | ||
| 225 | } | ||
| 226 | } catch (Exception ignored) { | ||
| 227 | price = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50; | ||
| 228 | } | ||
| 229 | ev.chart.add(new ChartPoint(time, Math.max(0.0, Math.min(1.0, price)))); | ||
| 230 | } | ||
| 231 | } else { | ||
| 232 | long now = Instant.now().getEpochSecond(); | ||
| 233 | double p = (ev.yesPrice != null) ? ev.yesPrice.doubleValue() : 0.50; | ||
| 234 | ev.chart.add(new ChartPoint(now - 60, p)); | ||
| 235 | ev.chart.add(new ChartPoint(now, p)); | ||
| 236 | } | ||
| 237 | |||
| 238 | // Order books for OB | ||
| 239 | if (ev.type == MechanismType.ORDER_BOOK) { | ||
| 240 | String yesLast = ev.yesPrice != null ? Format.money(ev.yesPrice) : "$0.50"; | ||
| 241 | String noLast = ev.noPrice != null ? Format.money(ev.noPrice) : "$0.50"; | ||
| 242 | ev.yesBook = new OrderBook("YES", true); | ||
| 243 | ev.yesBook.last = yesLast; | ||
| 244 | ev.yesBook.bid = "—"; | ||
| 245 | ev.yesBook.ask = "—"; | ||
| 246 | ev.yesBook.mid = yesLast; | ||
| 247 | ev.yesBook.spread = "—"; | ||
| 248 | |||
| 249 | ev.noBook = new OrderBook("NO", false); | ||
| 250 | ev.noBook.last = noLast; | ||
| 251 | ev.noBook.bid = "—"; | ||
| 252 | ev.noBook.ask = "—"; | ||
| 253 | ev.noBook.mid = noLast; | ||
| 254 | ev.noBook.spread = "—"; | ||
| 255 | |||
| 256 | if (detail != null && detail.orderBooks() != null && !detail.orderBooks().isEmpty()) { | ||
| 257 | for (var obDto : detail.orderBooks()) { | ||
| 258 | boolean isYes = | ||
| 259 | "YES".equalsIgnoreCase(obDto.optionKey()) | ||
| 260 | || "YES".equalsIgnoreCase(obDto.optionName()) | ||
| 261 | || obDto.optionKey().endsWith(":0") | ||
| 262 | || obDto.optionKey().equals("0"); | ||
| 263 | OrderBook target = isYes ? ev.yesBook : ev.noBook; | ||
| 264 | if (target != null) { | ||
| 265 | if (obDto.bestBid() != null) target.bid = obDto.bestBid(); | ||
| 266 | if (obDto.bestAsk() != null) target.ask = obDto.bestAsk(); | ||
| 267 | if (obDto.spread() != null) target.spread = obDto.spread(); | ||
| 268 | target.mid = midOf(target.bid, target.ask); | ||
| 269 | target.rows.clear(); | ||
| 270 | if (obDto.orders() != null) { | ||
| 271 | for (var rowDto : obDto.orders()) { | ||
| 272 | target.rows.add( | ||
| 273 | new BookRow( | ||
| 274 | rowDto.side(), | ||
| 275 | rowDto.user(), | ||
| 276 | (int) rowDto.quantity(), | ||
| 277 | rowDto.price(), | ||
| 278 | rowDto.isBid())); | ||
| 279 | } | ||
| 280 | } | ||
| 281 | } | ||
| 282 | } | ||
| 283 | } | ||
| 284 | } | ||
| 285 | |||
| 286 | // Participants | ||
| 287 | if (detail != null && detail.participants() != null && !detail.participants().isEmpty()) { | ||
| 288 | ev.participants.clear(); | ||
| 289 | for (var p : detail.participants()) { | ||
| 290 | ev.participants.add( | ||
| 291 | new ParticipantRow( | ||
| 292 | p.user(), p.tag(), p.yes(), p.no(), p.value(), p.fees(), p.pnl())); | ||
| 293 | } | ||
| 294 | } else if (ev.participants.isEmpty() && ev.mm != null && !ev.mm.isBlank()) { | ||
| 295 | ev.participants.add(new ParticipantRow(ev.mm, "MM", 0, 0, "$0.00", "$0.00", "$0.00")); | ||
| 296 | } | ||
| 297 | |||
| 298 | return ev; | ||
| 299 | } | ||
| 300 | |||
| 301 | private UserData toUserData(LedgerDTO acc, List<EventData> allEvents) { | ||
| 302 | double bal = 0.0; | ||
| 303 | try { | ||
| 304 | if (acc.balance() != null) { | ||
| 305 | bal = Double.parseDouble(acc.balance().replace("$", "").replace(",", "").trim()); | ||
| 306 | } | ||
| 307 | } catch (Exception ignored) { | ||
| 308 | } | ||
| 309 | |||
| 310 | boolean isMm = | ||
| 311 | allEvents.stream().anyMatch(e -> e.mm != null && acc.owner().equalsIgnoreCase(e.mm)); | ||
| 312 | String role = isMm ? "Market Maker" : "Trader"; | ||
| 313 | |||
| 314 | UserData u = new UserData(acc.owner(), role, bal, acc.blocked(), isMm); | ||
| 315 | |||
| 316 | if (acc.entries() != null && !acc.entries().isEmpty()) { | ||
| 317 | var entries = acc.entries(); | ||
| 318 | long firstT = parseEpochSecond(entries.get(0).at()); | ||
| 319 | if (firstT == 0L) firstT = Instant.now().getEpochSecond(); | ||
| 320 | |||
| 321 | double firstAmount = 0.0; | ||
| 322 | double firstBalAfter = 0.0; | ||
| 323 | try { | ||
| 324 | firstAmount = | ||
| 325 | Double.parseDouble(entries.get(0).amount().replace("$", "").replace(",", "").trim()); | ||
| 326 | firstBalAfter = | ||
| 327 | Double.parseDouble( | ||
| 328 | entries.get(0).balanceAfter().replace("$", "").replace(",", "").trim()); | ||
| 329 | } catch (Exception ignored) { | ||
| 330 | } | ||
| 331 | double initialBal = firstBalAfter - firstAmount; | ||
| 332 | u.balanceHistory.add(new ChartPoint(firstT - 60, Math.max(0.0, initialBal))); | ||
| 333 | |||
| 334 | for (var entry : entries) { | ||
| 335 | long t = parseEpochSecond(entry.at()); | ||
| 336 | if (t == 0L) t = firstT; | ||
| 337 | try { | ||
| 338 | double bAfter = | ||
| 339 | Double.parseDouble(entry.balanceAfter().replace("$", "").replace(",", "").trim()); | ||
| 340 | u.balanceHistory.add(new ChartPoint(t, bAfter)); | ||
| 341 | } catch (Exception ignored) { | ||
| 342 | } | ||
| 343 | } | ||
| 344 | } else { | ||
| 345 | long now = Instant.now().getEpochSecond(); | ||
| 346 | u.balanceHistory.add(new ChartPoint(now - 60, bal)); | ||
| 347 | u.balanceHistory.add(new ChartPoint(now, bal)); | ||
| 348 | } | ||
| 349 | |||
| 350 | for (var ev : allEvents) { | ||
| 351 | String typeStr = ev.type == MechanismType.LMSR ? "LMSR" : "Order Book"; | ||
| 352 | boolean isEvMm = ev.mm != null && acc.owner().equalsIgnoreCase(ev.mm); | ||
| 353 | long yesShares = 0; | ||
| 354 | long noShares = 0; | ||
| 355 | var pOpt = | ||
| 356 | ev.participants.stream().filter(p -> p.user().equalsIgnoreCase(acc.owner())).findFirst(); | ||
| 357 | if (pOpt.isPresent()) { | ||
| 358 | yesShares = pOpt.get().yes(); | ||
| 359 | noShares = pOpt.get().no(); | ||
| 360 | } else { | ||
| 361 | if (isEvMm && ev.type == MechanismType.ORDER_BOOK) { | ||
| 362 | yesShares += 100; | ||
| 363 | noShares += 100; | ||
| 364 | } | ||
| 365 | for (var trade : ev.trades) { | ||
| 366 | if (acc.owner().equalsIgnoreCase(trade.user())) { | ||
| 367 | if (trade.yesOption()) yesShares += trade.shares(); | ||
| 368 | else noShares += trade.shares(); | ||
| 369 | } | ||
| 370 | } | ||
| 371 | } | ||
| 372 | // Spec: P/L is reported once the event is closed; until then there is no realised result. | ||
| 373 | String valStr = | ||
| 374 | ev.status != EventStatus.SETTLED ? "—" : pOpt.isPresent() ? pOpt.get().pnl() : "$0.00"; | ||
| 375 | if (isEvMm) { | ||
| 376 | u.events.add( | ||
| 377 | new UserEventRow( | ||
| 378 | ev.id, | ||
| 379 | ev.name, | ||
| 380 | "Market Maker", | ||
| 381 | typeStr, | ||
| 382 | (int) Math.max(0, yesShares), | ||
| 383 | (int) Math.max(0, noShares), | ||
| 384 | valStr)); | ||
| 385 | } else if (yesShares > 0 || noShares > 0) { | ||
| 386 | u.events.add( | ||
| 387 | new UserEventRow( | ||
| 388 | ev.id, | ||
| 389 | ev.name, | ||
| 390 | "Trader", | ||
| 391 | typeStr, | ||
| 392 | (int) Math.max(0, yesShares), | ||
| 393 | (int) Math.max(0, noShares), | ||
| 394 | valStr)); | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | return u; | ||
| 399 | } | ||
| 400 | |||
| 401 | /** Mid price between best bid and best ask, or "—" when either side of the book is empty. */ | ||
| 402 | static String midOf(String bid, String ask) { | ||
| 403 | try { | ||
| 404 | var b = new BigDecimal(bid.replace("$", "").trim()); | ||
| 405 | var a = new BigDecimal(ask.replace("$", "").trim()); | ||
| 406 | return Format.money(b.add(a).divide(BigDecimal.TWO)); | ||
| 407 | } catch (RuntimeException e) { | ||
| 408 | return "—"; | ||
| 409 | } | ||
| 410 | } | ||
| 411 | |||
| 412 | public static long parseEpochSecond(String timeStr) { | ||
| 413 | if (timeStr == null || timeStr.isBlank()) return 0L; | ||
| 414 | try { | ||
| 415 | return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) | ||
| 416 | .atZone(ZoneId.systemDefault()) | ||
| 417 | .toEpochSecond(); | ||
| 418 | } catch (Exception e) { | ||
| 419 | try { | ||
| 420 | return Instant.parse(timeStr).getEpochSecond(); | ||
| 421 | } catch (Exception ignored) { | ||
| 422 | return 0L; | ||
| 423 | } | ||
| 424 | } | ||
| 425 | } | ||
| 426 | |||
| 427 | private void updateSelectedEvent() { | ||
| 428 | String id = selectedEventId.get(); | ||
| 429 | EventData ev = (id != null) ? event(id) : null; | ||
| 430 | if (ev == null && !events.isEmpty()) { | ||
| 431 | ev = events.get(0); | ||
| 432 | if (ev != null) { | ||
| 433 | selectedEventId.set(ev.id); | ||
| 434 | } | ||
| 435 | } | ||
| 436 | selectedEvent.set(null); | ||
| 437 | selectedEvent.set(ev); | ||
| 438 | } | ||
| 439 | |||
| 440 | private void updateActingUser() { | ||
| 441 | String name = actingUserName.get(); | ||
| 442 | UserData u = (name != null) ? user(name) : null; | ||
| 443 | if (u == null && !users.isEmpty()) { | ||
| 444 | u = users.get(0); | ||
| 445 | if (u != null) { | ||
| 446 | actingUserName.set(u.name); | ||
| 447 | } | ||
| 448 | } | ||
| 449 | actingUser.set(null); | ||
| 450 | actingUser.set(u); | ||
| 451 | } | ||
| 452 | |||
| 453 | public CatalogContext getCatalogContext() { | ||
| 454 | return catalogContext; | ||
| 455 | } | ||
| 456 | |||
| 457 | public GuessMarketContext getMarketContext() { | ||
| 458 | return marketContext; | ||
| 459 | } | ||
| 460 | |||
| 461 | public AccountContext getAccountContext() { | ||
| 462 | return accountContext; | ||
| 463 | } | ||
| 464 | |||
| 465 | public ObservableList<EventData> getEvents() { | ||
| 466 | return events; | ||
| 467 | } | ||
| 468 | |||
| 469 | public ObservableList<UserData> getUsers() { | ||
| 470 | return users; | ||
| 471 | } | ||
| 472 | |||
| 473 | public StringProperty loadedFileProperty() { | ||
| 474 | return loadedFile; | ||
| 475 | } | ||
| 476 | |||
| 477 | public String getLoadedFile() { | ||
| 478 | return loadedFile.get(); | ||
| 479 | } | ||
| 480 | |||
| 481 | public void setLoadedFile(String file) { | ||
| 482 | this.loadedFile.set(file); | ||
| 483 | } | ||
| 484 | |||
| 485 | public StringProperty selectedEventIdProperty() { | ||
| 486 | return selectedEventId; | ||
| 487 | } | ||
| 488 | |||
| 489 | public String getSelectedEventId() { | ||
| 490 | return selectedEventId.get(); | ||
| 491 | } | ||
| 492 | |||
| 493 | public void setSelectedEventId(String id) { | ||
| 494 | this.selectedEventId.set(id); | ||
| 495 | } | ||
| 496 | |||
| 497 | public ObjectProperty<EventData> selectedEventProperty() { | ||
| 498 | return selectedEvent; | ||
| 499 | } | ||
| 500 | |||
| 501 | public EventData selectedEvent() { | ||
| 502 | return selectedEvent.get(); | ||
| 503 | } | ||
| 504 | |||
| 505 | public EventData getSelectedEvent() { | ||
| 506 | return selectedEvent.get(); | ||
| 507 | } | ||
| 508 | |||
| 509 | public void setSelectedEvent(EventData e) { | ||
| 510 | this.selectedEvent.set(e); | ||
| 511 | if (e != null) { | ||
| 512 | this.selectedEventId.set(e.id); | ||
| 513 | } | ||
| 514 | } | ||
| 515 | |||
| 516 | public StringProperty actingUserNameProperty() { | ||
| 517 | return actingUserName; | ||
| 518 | } | ||
| 519 | |||
| 520 | public String getActingUserName() { | ||
| 521 | return actingUserName.get(); | ||
| 522 | } | ||
| 523 | |||
| 524 | public void setActingUserName(String name) { | ||
| 525 | this.actingUserName.set(name); | ||
| 526 | } | ||
| 527 | |||
| 528 | public ObjectProperty<UserData> actingUserProperty() { | ||
| 529 | return actingUser; | ||
| 530 | } | ||
| 531 | |||
| 532 | public UserData actingUser() { | ||
| 533 | return actingUser.get(); | ||
| 534 | } | ||
| 535 | |||
| 536 | public UserData getActingUser() { | ||
| 537 | return actingUser.get(); | ||
| 538 | } | ||
| 539 | |||
| 540 | public void setActingUser(UserData u) { | ||
| 541 | this.actingUser.set(u); | ||
| 542 | if (u != null) { | ||
| 543 | this.actingUserName.set(u.name); | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 547 | public StringProperty selectedUserEventIdProperty() { | ||
| 548 | return selectedUserEventId; | ||
| 549 | } | ||
| 550 | |||
| 551 | public String getSelectedUserEventId() { | ||
| 552 | return selectedUserEventId.get(); | ||
| 553 | } | ||
| 554 | |||
| 555 | public void setSelectedUserEventId(String id) { | ||
| 556 | this.selectedUserEventId.set(id); | ||
| 557 | } | ||
| 558 | |||
| 559 | public ObjectProperty<MechanismFilter> filterMethodProperty() { | ||
| 560 | return filterMethod; | ||
| 561 | } | ||
| 562 | |||
| 563 | public MechanismFilter getFilterMethod() { | ||
| 564 | return filterMethod.get(); | ||
| 565 | } | ||
| 566 | |||
| 567 | public void setFilterMethod(MechanismFilter m) { | ||
| 568 | this.filterMethod.set(m); | ||
| 569 | } | ||
| 570 | |||
| 571 | public ObjectProperty<EventStatus> filterStatusProperty() { | ||
| 572 | return filterStatus; | ||
| 573 | } | ||
| 574 | |||
| 575 | public EventStatus getFilterStatus() { | ||
| 576 | return filterStatus.get(); | ||
| 577 | } | ||
| 578 | |||
| 579 | public void setFilterStatus(EventStatus s) { | ||
| 580 | this.filterStatus.set(s); | ||
| 581 | } | ||
| 582 | |||
| 583 | public ObjectProperty<CommissionFilter> filterFeeProperty() { | ||
| 584 | return filterFee; | ||
| 585 | } | ||
| 586 | |||
| 587 | public CommissionFilter getFilterFee() { | ||
| 588 | return filterFee.get(); | ||
| 589 | } | ||
| 590 | |||
| 591 | public void setFilterFee(CommissionFilter f) { | ||
| 592 | this.filterFee.set(f); | ||
| 593 | } | ||
| 594 | |||
| 595 | public ObjectProperty<TradeSide> tradeSideProperty() { | ||
| 596 | return tradeSide; | ||
| 597 | } | ||
| 598 | |||
| 599 | public TradeSide getTradeSide() { | ||
| 600 | return tradeSide.get(); | ||
| 601 | } | ||
| 602 | |||
| 603 | public void setTradeSide(TradeSide s) { | ||
| 604 | this.tradeSide.set(s); | ||
| 605 | } | ||
| 606 | |||
| 607 | public StringProperty tradeQtyProperty() { | ||
| 608 | return tradeQty; | ||
| 609 | } | ||
| 610 | |||
| 611 | public String getTradeQty() { | ||
| 612 | return tradeQty.get(); | ||
| 613 | } | ||
| 614 | |||
| 615 | public void setTradeQty(String q) { | ||
| 616 | this.tradeQty.set(q); | ||
| 617 | } | ||
| 618 | |||
| 619 | public StringProperty tradePriceProperty() { | ||
| 620 | return tradePrice; | ||
| 621 | } | ||
| 622 | |||
| 623 | public String getTradePrice() { | ||
| 624 | return tradePrice.get(); | ||
| 625 | } | ||
| 626 | |||
| 627 | public void setTradePrice(String p) { | ||
| 628 | this.tradePrice.set(p); | ||
| 629 | } | ||
| 630 | |||
| 631 | public BooleanProperty tradeOptionYesProperty() { | ||
| 632 | return tradeOptionYes; | ||
| 633 | } | ||
| 634 | |||
| 635 | public boolean isTradeOptionYes() { | ||
| 636 | return tradeOptionYes.get(); | ||
| 637 | } | ||
| 638 | |||
| 639 | public void setTradeOptionYes(boolean y) { | ||
| 640 | this.tradeOptionYes.set(y); | ||
| 641 | } | ||
| 642 | |||
| 643 | public ObjectProperty<Skin> skinProperty() { | ||
| 644 | return skin; | ||
| 645 | } | ||
| 646 | |||
| 647 | public Skin getSkin() { | ||
| 648 | return skin.get(); | ||
| 649 | } | ||
| 650 | |||
| 651 | public void setSkin(Skin s) { | ||
| 652 | this.skin.set(s); | ||
| 653 | } | ||
| 654 | |||
| 655 | public BooleanProperty animationsOnProperty() { | ||
| 656 | return animationsOn; | ||
| 657 | } | ||
| 658 | |||
| 659 | public boolean isAnimationsOn() { | ||
| 660 | return animationsOn.get(); | ||
| 661 | } | ||
| 662 | |||
| 663 | public void setAnimationsOn(boolean a) { | ||
| 664 | this.animationsOn.set(a); | ||
| 665 | } | ||
| 666 | |||
| 667 | public ObjectProperty<AppTab> activeTabProperty() { | ||
| 668 | return activeTab; | ||
| 669 | } | ||
| 670 | |||
| 671 | public AppTab getActiveTab() { | ||
| 672 | return activeTab.get(); | ||
| 673 | } | ||
| 674 | |||
| 675 | public void setActiveTab(AppTab t) { | ||
| 676 | this.activeTab.set(t); | ||
| 677 | } | ||
| 678 | |||
| 679 | public ObjectProperty<ModalDialog> dialogProperty() { | ||
| 680 | return dialog; | ||
| 681 | } | ||
| 682 | |||
| 683 | public ModalDialog getDialog() { | ||
| 684 | return dialog.get(); | ||
| 685 | } | ||
| 686 | |||
| 687 | public void setDialog(ModalDialog d) { | ||
| 688 | this.dialog.set(d); | ||
| 689 | } | ||
| 690 | |||
| 691 | public ObjectProperty<MechanismType> createTypeProperty() { | ||
| 692 | return createType; | ||
| 693 | } | ||
| 694 | |||
| 695 | public MechanismType getCreateType() { | ||
| 696 | return createType.get(); | ||
| 697 | } | ||
| 698 | |||
| 699 | public void setCreateType(MechanismType t) { | ||
| 700 | this.createType.set(t); | ||
| 701 | } | ||
| 702 | |||
| 703 | public BooleanProperty createMintingOnProperty() { | ||
| 704 | return createMintingOn; | ||
| 705 | } | ||
| 706 | |||
| 707 | public boolean isCreateMintingOn() { | ||
| 708 | return createMintingOn.get(); | ||
| 709 | } | ||
| 710 | |||
| 711 | public void setCreateMintingOn(boolean m) { | ||
| 712 | this.createMintingOn.set(m); | ||
| 713 | } | ||
| 714 | |||
| 715 | public StringProperty resolveSelectedOptionProperty() { | ||
| 716 | return resolveSelectedOption; | ||
| 717 | } | ||
| 718 | |||
| 719 | public String getResolveSelectedOption() { | ||
| 720 | return resolveSelectedOption.get(); | ||
| 721 | } | ||
| 722 | |||
| 723 | public void setResolveSelectedOption(String o) { | ||
| 724 | this.resolveSelectedOption.set(o); | ||
| 725 | } | ||
| 726 | |||
| 727 | public StringProperty toastProperty() { | ||
| 728 | return toast; | ||
| 729 | } | ||
| 730 | |||
| 731 | public String getToast() { | ||
| 732 | return toast.get(); | ||
| 733 | } | ||
| 734 | |||
| 735 | public void setToast(String t) { | ||
| 736 | this.toast.set(t); | ||
| 737 | } | ||
| 738 | |||
| 739 | public StringProperty pendingFileProperty() { | ||
| 740 | return pendingFile; | ||
| 741 | } | ||
| 742 | |||
| 743 | public String getPendingFile() { | ||
| 744 | return pendingFile.get(); | ||
| 745 | } | ||
| 746 | |||
| 747 | public void setPendingFile(String f) { | ||
| 748 | this.pendingFile.set(f); | ||
| 749 | } | ||
| 750 | |||
| 751 | public EventData event(String id) { | ||
| 752 | if (id == null) return null; | ||
| 753 | return events.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null); | ||
| 754 | } | ||
| 755 | |||
| 756 | public UserData user(String name) { | ||
| 757 | if (name == null) return null; | ||
| 758 | return users.stream().filter(u -> u.getName().equals(name)).findFirst().orElse(null); | ||
| 759 | } | ||
| 760 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java new file mode 100644 index 0000000..f5cf26f --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/AppView.java | |||
| @@ -0,0 +1,260 @@ | |||
| 1 | package market.guess.ui.desktop; | ||
| 2 | |||
| 3 | import javafx.fxml.FXMLLoader; | ||
| 4 | import javafx.geometry.Pos; | ||
| 5 | import javafx.scene.Cursor; | ||
| 6 | import javafx.scene.Scene; | ||
| 7 | import javafx.scene.control.ScrollPane; | ||
| 8 | import javafx.scene.layout.Region; | ||
| 9 | import javafx.scene.layout.StackPane; | ||
| 10 | import javafx.scene.paint.Color; | ||
| 11 | import javafx.scene.shape.Rectangle; | ||
| 12 | import javafx.stage.Stage; | ||
| 13 | import javafx.stage.StageStyle; | ||
| 14 | import market.guess.api.AccountContext; | ||
| 15 | import market.guess.api.CatalogContext; | ||
| 16 | import market.guess.api.GuessMarketContext; | ||
| 17 | import market.guess.ui.desktop.controllers.AppController; | ||
| 18 | |||
| 19 | public final class AppView { | ||
| 20 | private static final double CONTENT_MIN_WIDTH = 1040; | ||
| 21 | private static final double CONTENT_MIN_HEIGHT = 560; | ||
| 22 | private static final double WINDOW_MIN_WIDTH = 400; | ||
| 23 | private static final double WINDOW_MIN_HEIGHT = 300; | ||
| 24 | |||
| 25 | private final Stage stage; | ||
| 26 | private final CatalogContext catalogContext; | ||
| 27 | private final GuessMarketContext marketContext; | ||
| 28 | private final AccountContext accountContext; | ||
| 29 | private final AppState state; | ||
| 30 | private AppController controller; | ||
| 31 | |||
| 32 | public AppView(Stage stage) { | ||
| 33 | this(stage, new ServiceEngine()); | ||
| 34 | } | ||
| 35 | |||
| 36 | public AppView(Stage stage, ServiceEngine engine) { | ||
| 37 | this(stage, engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext()); | ||
| 38 | } | ||
| 39 | |||
| 40 | public AppView( | ||
| 41 | Stage stage, | ||
| 42 | CatalogContext catalogContext, | ||
| 43 | GuessMarketContext marketContext, | ||
| 44 | AccountContext accountContext) { | ||
| 45 | this.stage = stage; | ||
| 46 | this.catalogContext = catalogContext; | ||
| 47 | this.marketContext = marketContext; | ||
| 48 | this.accountContext = accountContext; | ||
| 49 | this.state = new AppState(catalogContext, marketContext, accountContext); | ||
| 50 | stage.initStyle(StageStyle.TRANSPARENT); | ||
| 51 | initScene(); | ||
| 52 | } | ||
| 53 | |||
| 54 | public CatalogContext getCatalogContext() { | ||
| 55 | return catalogContext; | ||
| 56 | } | ||
| 57 | |||
| 58 | public GuessMarketContext getMarketContext() { | ||
| 59 | return marketContext; | ||
| 60 | } | ||
| 61 | |||
| 62 | public AccountContext getAccountContext() { | ||
| 63 | return accountContext; | ||
| 64 | } | ||
| 65 | |||
| 66 | private void initScene() { | ||
| 67 | try { | ||
| 68 | var loader = new FXMLLoader(getClass().getResource("app_view.fxml")); | ||
| 69 | StackPane root = loader.load(); | ||
| 70 | this.controller = loader.getController(); | ||
| 71 | this.controller.init(this); | ||
| 72 | |||
| 73 | var clip = new Rectangle(); | ||
| 74 | clip.setArcWidth(22); | ||
| 75 | clip.setArcHeight(22); | ||
| 76 | clip.widthProperty().bind(root.widthProperty()); | ||
| 77 | clip.heightProperty().bind(root.heightProperty()); | ||
| 78 | root.setClip(clip); | ||
| 79 | |||
| 80 | addResizeHandles(root); | ||
| 81 | |||
| 82 | var scene = new Scene(root, 1280, 800); | ||
| 83 | scene.setFill(Color.TRANSPARENT); | ||
| 84 | scene.getStylesheets().add(getClass().getResource("theme.css").toExternalForm()); | ||
| 85 | stage.setScene(scene); | ||
| 86 | stage.setTitle("Guess Market"); | ||
| 87 | stage.setMinWidth(WINDOW_MIN_WIDTH); | ||
| 88 | stage.setMinHeight(WINDOW_MIN_HEIGHT); | ||
| 89 | // lookup() can't see ScrollPane content until its skin exists, so go through getContent(). | ||
| 90 | var appScroll = (ScrollPane) root.lookup(".gm-app-scroll"); | ||
| 91 | ((Region) appScroll.getContent()).setMinSize(CONTENT_MIN_WIDTH, CONTENT_MIN_HEIGHT); | ||
| 92 | |||
| 93 | controller.refresh(); | ||
| 94 | } catch (Exception e) { | ||
| 95 | throw new RuntimeException("Failed to load app_view.fxml", e); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | public Stage getStage() { | ||
| 100 | return stage; | ||
| 101 | } | ||
| 102 | |||
| 103 | public AppState getState() { | ||
| 104 | return state; | ||
| 105 | } | ||
| 106 | |||
| 107 | public void show() { | ||
| 108 | stage.show(); | ||
| 109 | } | ||
| 110 | |||
| 111 | private static final double RESIZE_MARGIN = 6; | ||
| 112 | private static final double RESIZE_CORNER = 12; | ||
| 113 | |||
| 114 | private void addResizeHandles(StackPane stack) { | ||
| 115 | stack | ||
| 116 | .getChildren() | ||
| 117 | .addAll( | ||
| 118 | resizeHandle( | ||
| 119 | stack, | ||
| 120 | Cursor.N_RESIZE, | ||
| 121 | Pos.TOP_CENTER, | ||
| 122 | -1, | ||
| 123 | RESIZE_MARGIN, | ||
| 124 | false, | ||
| 125 | false, | ||
| 126 | true, | ||
| 127 | false), | ||
| 128 | resizeHandle( | ||
| 129 | stack, | ||
| 130 | Cursor.S_RESIZE, | ||
| 131 | Pos.BOTTOM_CENTER, | ||
| 132 | -1, | ||
| 133 | RESIZE_MARGIN, | ||
| 134 | false, | ||
| 135 | false, | ||
| 136 | false, | ||
| 137 | true), | ||
| 138 | resizeHandle( | ||
| 139 | stack, | ||
| 140 | Cursor.W_RESIZE, | ||
| 141 | Pos.CENTER_LEFT, | ||
| 142 | RESIZE_MARGIN, | ||
| 143 | -1, | ||
| 144 | true, | ||
| 145 | false, | ||
| 146 | false, | ||
| 147 | false), | ||
| 148 | resizeHandle( | ||
| 149 | stack, | ||
| 150 | Cursor.E_RESIZE, | ||
| 151 | Pos.CENTER_RIGHT, | ||
| 152 | RESIZE_MARGIN, | ||
| 153 | -1, | ||
| 154 | false, | ||
| 155 | true, | ||
| 156 | false, | ||
| 157 | false), | ||
| 158 | resizeHandle( | ||
| 159 | stack, | ||
| 160 | Cursor.NW_RESIZE, | ||
| 161 | Pos.TOP_LEFT, | ||
| 162 | RESIZE_CORNER, | ||
| 163 | RESIZE_CORNER, | ||
| 164 | true, | ||
| 165 | false, | ||
| 166 | true, | ||
| 167 | false), | ||
| 168 | resizeHandle( | ||
| 169 | stack, | ||
| 170 | Cursor.NE_RESIZE, | ||
| 171 | Pos.TOP_RIGHT, | ||
| 172 | RESIZE_CORNER, | ||
| 173 | RESIZE_CORNER, | ||
| 174 | false, | ||
| 175 | true, | ||
| 176 | true, | ||
| 177 | false), | ||
| 178 | resizeHandle( | ||
| 179 | stack, | ||
| 180 | Cursor.SW_RESIZE, | ||
| 181 | Pos.BOTTOM_LEFT, | ||
| 182 | RESIZE_CORNER, | ||
| 183 | RESIZE_CORNER, | ||
| 184 | true, | ||
| 185 | false, | ||
| 186 | false, | ||
| 187 | true), | ||
| 188 | resizeHandle( | ||
| 189 | stack, | ||
| 190 | Cursor.SE_RESIZE, | ||
| 191 | Pos.BOTTOM_RIGHT, | ||
| 192 | RESIZE_CORNER, | ||
| 193 | RESIZE_CORNER, | ||
| 194 | false, | ||
| 195 | true, | ||
| 196 | false, | ||
| 197 | true)); | ||
| 198 | } | ||
| 199 | |||
| 200 | private Region resizeHandle( | ||
| 201 | StackPane stack, | ||
| 202 | Cursor cursor, | ||
| 203 | Pos pos, | ||
| 204 | double w, | ||
| 205 | double h, | ||
| 206 | boolean left, | ||
| 207 | boolean right, | ||
| 208 | boolean top, | ||
| 209 | boolean bottom) { | ||
| 210 | var r = new Region(); | ||
| 211 | r.setCursor(cursor); | ||
| 212 | r.setMouseTransparent(false); | ||
| 213 | if (w < 0) { | ||
| 214 | r.prefWidthProperty().bind(stack.widthProperty()); | ||
| 215 | } else { | ||
| 216 | r.setPrefWidth(w); | ||
| 217 | r.setMaxWidth(w); | ||
| 218 | } | ||
| 219 | if (h < 0) { | ||
| 220 | r.prefHeightProperty().bind(stack.heightProperty()); | ||
| 221 | } else { | ||
| 222 | r.setPrefHeight(h); | ||
| 223 | r.setMaxHeight(h); | ||
| 224 | } | ||
| 225 | StackPane.setAlignment(r, pos); | ||
| 226 | |||
| 227 | double[] start = new double[6]; | ||
| 228 | r.setOnMousePressed( | ||
| 229 | e -> { | ||
| 230 | if (stage.isMaximized()) return; | ||
| 231 | start[0] = e.getScreenX(); | ||
| 232 | start[1] = e.getScreenY(); | ||
| 233 | start[2] = stage.getX(); | ||
| 234 | start[3] = stage.getY(); | ||
| 235 | start[4] = stage.getWidth(); | ||
| 236 | start[5] = stage.getHeight(); | ||
| 237 | e.consume(); | ||
| 238 | }); | ||
| 239 | r.setOnMouseDragged( | ||
| 240 | e -> { | ||
| 241 | if (stage.isMaximized()) return; | ||
| 242 | double dx = e.getScreenX() - start[0]; | ||
| 243 | double dy = e.getScreenY() - start[1]; | ||
| 244 | if (right) stage.setWidth(Math.max(stage.getMinWidth(), start[4] + dx)); | ||
| 245 | if (bottom) stage.setHeight(Math.max(stage.getMinHeight(), start[5] + dy)); | ||
| 246 | if (left) { | ||
| 247 | double newW = Math.max(stage.getMinWidth(), start[4] - dx); | ||
| 248 | stage.setX(start[2] + (start[4] - newW)); | ||
| 249 | stage.setWidth(newW); | ||
| 250 | } | ||
| 251 | if (top) { | ||
| 252 | double newH = Math.max(stage.getMinHeight(), start[5] - dy); | ||
| 253 | stage.setY(start[3] + (start[5] - newH)); | ||
| 254 | stage.setHeight(newH); | ||
| 255 | } | ||
| 256 | e.consume(); | ||
| 257 | }); | ||
| 258 | return r; | ||
| 259 | } | ||
| 260 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java b/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java new file mode 100644 index 0000000..549991e --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/ServiceEngine.java | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | package market.guess.ui.desktop; | ||
| 2 | |||
| 3 | import java.time.Clock; | ||
| 4 | import market.guess.api.AccountContext; | ||
| 5 | import market.guess.api.CatalogContext; | ||
| 6 | import market.guess.api.GuessMarketContext; | ||
| 7 | import market.guess.service.LocalGuessMarketContext; | ||
| 8 | import market.guess.service.catalog.LocalCatalogContext; | ||
| 9 | import market.guess.service.catalog.infrastructure.MarketContext; | ||
| 10 | import market.guess.service.catalog.infrastructure.mapper.v2.EventMapperV2; | ||
| 11 | import market.guess.service.catalog.infrastructure.provider.Loader; | ||
| 12 | import market.guess.service.catalog.infrastructure.provider.v2.XMLLoaderV2; | ||
| 13 | import market.guess.service.catalog.infrastructure.provider.v2.XMLValidatorV2; | ||
| 14 | import market.guess.service.catalog.infrastructure.repository.EventRepository; | ||
| 15 | import market.guess.service.catalog.infrastructure.repository.InMemoryEventRepository; | ||
| 16 | import market.guess.service.catalog.infrastructure.repository.InMemoryUserRepository; | ||
| 17 | import market.guess.service.catalog.infrastructure.repository.UserRepository; | ||
| 18 | import market.guess.service.fulfillment.FulfillmentContext; | ||
| 19 | import market.guess.service.fulfillment.LocalFulfillmentContext; | ||
| 20 | import market.guess.service.ledger.LedgerContext; | ||
| 21 | import market.guess.service.ledger.LocalAccountContext; | ||
| 22 | import market.guess.service.matching.LocalMatchingEngine; | ||
| 23 | import market.guess.service.matching.MatchingEngine; | ||
| 24 | import market.guess.service.risk.LocalRiskEngine; | ||
| 25 | import market.guess.service.risk.RiskEngine; | ||
| 26 | import market.guess.service.settlement.LocalSettlementContext; | ||
| 27 | import market.guess.service.settlement.SettlementContext; | ||
| 28 | |||
| 29 | public final class ServiceEngine { | ||
| 30 | private final CatalogContext catalogContext; | ||
| 31 | private final GuessMarketContext marketContext; | ||
| 32 | private final AccountContext accountContext; | ||
| 33 | |||
| 34 | public ServiceEngine() { | ||
| 35 | this(Clock.systemUTC()); | ||
| 36 | } | ||
| 37 | |||
| 38 | public ServiceEngine(Clock clock) { | ||
| 39 | EventRepository eventRepo = new InMemoryEventRepository(); | ||
| 40 | UserRepository userRepo = new InMemoryUserRepository(); | ||
| 41 | MarketContext marketContextInfrastructure = new MarketContext(eventRepo, userRepo); | ||
| 42 | EventMapperV2 mapper = new EventMapperV2(); | ||
| 43 | XMLValidatorV2 validator = new XMLValidatorV2(); | ||
| 44 | Loader loader = new XMLLoaderV2(eventRepo, userRepo, mapper, validator); | ||
| 45 | |||
| 46 | this.catalogContext = new LocalCatalogContext(loader, marketContextInfrastructure, eventRepo); | ||
| 47 | this.accountContext = new LocalAccountContext(userRepo); | ||
| 48 | |||
| 49 | LedgerContext ledgerContext = new LedgerContext(userRepo, clock); | ||
| 50 | RiskEngine risk = new LocalRiskEngine(eventRepo); | ||
| 51 | MatchingEngine matching = new LocalMatchingEngine(clock); | ||
| 52 | FulfillmentContext fulfillment = new LocalFulfillmentContext(ledgerContext); | ||
| 53 | SettlementContext settlement = new LocalSettlementContext(ledgerContext); | ||
| 54 | |||
| 55 | this.marketContext = | ||
| 56 | new LocalGuessMarketContext( | ||
| 57 | marketContextInfrastructure, risk, matching, fulfillment, settlement); | ||
| 58 | } | ||
| 59 | |||
| 60 | public CatalogContext getCatalogContext() { | ||
| 61 | return catalogContext; | ||
| 62 | } | ||
| 63 | |||
| 64 | public GuessMarketContext getMarketContext() { | ||
| 65 | return marketContext; | ||
| 66 | } | ||
| 67 | |||
| 68 | public AccountContext getAccountContext() { | ||
| 69 | return accountContext; | ||
| 70 | } | ||
| 71 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java b/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java new file mode 100644 index 0000000..b8e12d7 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/Skin.java | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | package market.guess.ui.desktop; | ||
| 2 | |||
| 3 | public enum Skin { | ||
| 4 | ROSE_PINE_DAWN("Rosé Pine Dawn", "CaskaydiaCove NF"), | ||
| 5 | CATPPUCCIN("Catppuccin", "JetBrainsMono NF"), | ||
| 6 | GRUVBOX("Gruvbox", "FiraCode Nerd Font"); | ||
| 7 | |||
| 8 | private final String displayName; | ||
| 9 | private final String fontFamily; | ||
| 10 | |||
| 11 | Skin(String displayName, String fontFamily) { | ||
| 12 | this.displayName = displayName; | ||
| 13 | this.fontFamily = fontFamily; | ||
| 14 | } | ||
| 15 | |||
| 16 | public String getDisplayName() { | ||
| 17 | return displayName; | ||
| 18 | } | ||
| 19 | |||
| 20 | public String getFontFamily() { | ||
| 21 | return fontFamily; | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java new file mode 100644 index 0000000..c17a0a6 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/AppTab.java | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | public enum AppTab { | ||
| 4 | EVENTS, | ||
| 5 | USERS | ||
| 6 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java new file mode 100644 index 0000000..333a544 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/CommissionFilter.java | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | public enum CommissionFilter { | ||
| 4 | ALL, | ||
| 5 | PURCHASE, | ||
| 6 | CLOSE | ||
| 7 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java new file mode 100644 index 0000000..4f5bb53 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListCell.java | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.scene.control.ContentDisplay; | ||
| 4 | import javafx.scene.control.ListCell; | ||
| 5 | import market.guess.ui.desktop.model.EventData; | ||
| 6 | |||
| 7 | public class EventListCell extends ListCell<EventData> { | ||
| 8 | private final EventListItemView view = new EventListItemView(); | ||
| 9 | |||
| 10 | public EventListCell() { | ||
| 11 | setContentDisplay(ContentDisplay.GRAPHIC_ONLY); | ||
| 12 | selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { | ||
| 13 | if (getItem() != null) { | ||
| 14 | view.update(getItem(), isNowSelected); | ||
| 15 | } | ||
| 16 | }); | ||
| 17 | } | ||
| 18 | |||
| 19 | @Override | ||
| 20 | protected void updateItem(EventData item, boolean empty) { | ||
| 21 | super.updateItem(item, empty); | ||
| 22 | if (empty || item == null) { | ||
| 23 | setGraphic(null); | ||
| 24 | } else { | ||
| 25 | view.update(item, isSelected()); | ||
| 26 | setGraphic(view); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java new file mode 100644 index 0000000..892dbfc --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/EventListItemView.java | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.css.PseudoClass; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.VBox; | ||
| 7 | import market.guess.model.event.EventStatus; | ||
| 8 | import market.guess.model.event.MechanismType; | ||
| 9 | import market.guess.ui.desktop.model.EventData; | ||
| 10 | import market.guess.ui.desktop.util.Format; | ||
| 11 | import market.guess.ui.desktop.util.Views; | ||
| 12 | |||
| 13 | public class EventListItemView extends VBox { | ||
| 14 | private static final PseudoClass ACTIVE = PseudoClass.getPseudoClass("active"); | ||
| 15 | private static final PseudoClass CLOSED = PseudoClass.getPseudoClass("closed"); | ||
| 16 | private static final PseudoClass IDLE = PseudoClass.getPseudoClass("idle"); | ||
| 17 | |||
| 18 | @FXML private Label numLabel; | ||
| 19 | @FXML private Label titleLabel; | ||
| 20 | @FXML private Label typeLabel; | ||
| 21 | @FXML private Label statusLabel; | ||
| 22 | @FXML private Label feeLabel; | ||
| 23 | @FXML private Label contractLabel; | ||
| 24 | |||
| 25 | public EventListItemView() { | ||
| 26 | Views.loadRoot(this, "event_list_item.fxml"); | ||
| 27 | } | ||
| 28 | |||
| 29 | public EventListItemView(EventData e, boolean selected) { | ||
| 30 | this(); | ||
| 31 | update(e, selected); | ||
| 32 | } | ||
| 33 | |||
| 34 | public void update(EventData e, boolean selected) { | ||
| 35 | numLabel.setText("#" + e.num); | ||
| 36 | titleLabel.setText(e.name); | ||
| 37 | typeLabel.setText(e.type == MechanismType.LMSR ? "LMSR" : "Order Book"); | ||
| 38 | |||
| 39 | statusLabel.setText( | ||
| 40 | e.status == EventStatus.ACTIVE | ||
| 41 | ? "ACTIVE" | ||
| 42 | : e.status == EventStatus.SETTLED ? "CLOSED" : "IDLE"); | ||
| 43 | statusLabel.pseudoClassStateChanged(ACTIVE, e.status == EventStatus.ACTIVE); | ||
| 44 | statusLabel.pseudoClassStateChanged(CLOSED, e.status == EventStatus.SETTLED); | ||
| 45 | statusLabel.pseudoClassStateChanged(IDLE, e.status == EventStatus.DRAFT); | ||
| 46 | |||
| 47 | feeLabel.setText(e.feeText()); | ||
| 48 | contractLabel.setText(Format.money(e.contract)); | ||
| 49 | Views.setSelected(this, selected); | ||
| 50 | } | ||
| 51 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java new file mode 100644 index 0000000..e2ecfbd --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/LadderItemView.java | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.css.PseudoClass; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.HBox; | ||
| 7 | import market.guess.ui.desktop.model.BookRow; | ||
| 8 | import market.guess.ui.desktop.util.Views; | ||
| 9 | |||
| 10 | public class LadderItemView extends HBox { | ||
| 11 | private static final PseudoClass BID = PseudoClass.getPseudoClass("bid"); | ||
| 12 | private static final PseudoClass ASK = PseudoClass.getPseudoClass("ask"); | ||
| 13 | |||
| 14 | @FXML private Label priceLabel; | ||
| 15 | @FXML private Label sideLabel; | ||
| 16 | @FXML private Label qtyLabel; | ||
| 17 | @FXML private Label userLabel; | ||
| 18 | |||
| 19 | public LadderItemView() { | ||
| 20 | Views.loadRoot(this, "ladder_item.fxml"); | ||
| 21 | } | ||
| 22 | |||
| 23 | public LadderItemView(BookRow row) { | ||
| 24 | this(); | ||
| 25 | update(row); | ||
| 26 | } | ||
| 27 | |||
| 28 | public void update(BookRow r) { | ||
| 29 | priceLabel.setText(r.price()); | ||
| 30 | sideLabel.setText(r.side()); | ||
| 31 | qtyLabel.setText(String.valueOf(r.qty())); | ||
| 32 | userLabel.setText(r.user()); | ||
| 33 | |||
| 34 | pseudoClassStateChanged(BID, r.isBid()); | ||
| 35 | pseudoClassStateChanged(ASK, !r.isBid()); | ||
| 36 | } | ||
| 37 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java new file mode 100644 index 0000000..7a85304 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/MechanismFilter.java | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | public enum MechanismFilter { | ||
| 4 | ALL, | ||
| 5 | LMSR, | ||
| 6 | ORDER_BOOK | ||
| 7 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java new file mode 100644 index 0000000..21865a5 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ModalDialog.java | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | public enum ModalDialog { | ||
| 4 | NONE, | ||
| 5 | LOAD, | ||
| 6 | CREATE_EVENT, | ||
| 7 | RESOLVE | ||
| 8 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java new file mode 100644 index 0000000..2161cfe --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/ParticipantItemView.java | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.fxml.FXML; | ||
| 4 | import javafx.scene.control.Label; | ||
| 5 | import javafx.scene.layout.HBox; | ||
| 6 | import market.guess.ui.desktop.model.ParticipantRow; | ||
| 7 | import market.guess.ui.desktop.util.Views; | ||
| 8 | |||
| 9 | public class ParticipantItemView extends HBox { | ||
| 10 | @FXML private Label userLabel; | ||
| 11 | @FXML private Label yesLabel; | ||
| 12 | @FXML private Label noLabel; | ||
| 13 | @FXML private Label valueLabel; | ||
| 14 | @FXML private Label feesLabel; | ||
| 15 | |||
| 16 | public ParticipantItemView() { | ||
| 17 | Views.loadRoot(this, "participant_item.fxml"); | ||
| 18 | } | ||
| 19 | |||
| 20 | public ParticipantItemView(ParticipantRow p) { | ||
| 21 | this(); | ||
| 22 | update(p); | ||
| 23 | } | ||
| 24 | |||
| 25 | public void update(ParticipantRow p) { | ||
| 26 | userLabel.setText(p.user() + (p.tag().isEmpty() ? "" : " (" + p.tag() + ")")); | ||
| 27 | yesLabel.setText(String.valueOf(p.yes())); | ||
| 28 | noLabel.setText(String.valueOf(p.no())); | ||
| 29 | valueLabel.setText(p.value()); | ||
| 30 | feesLabel.setText(p.fees()); | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java new file mode 100644 index 0000000..f00090c --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeHistoryItemView.java | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.css.PseudoClass; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.HBox; | ||
| 7 | import market.guess.ui.desktop.model.TradeRow; | ||
| 8 | import market.guess.ui.desktop.util.Views; | ||
| 9 | |||
| 10 | public class TradeHistoryItemView extends HBox { | ||
| 11 | private static final PseudoClass YES_STATE = PseudoClass.getPseudoClass("yes"); | ||
| 12 | private static final PseudoClass NO_STATE = PseudoClass.getPseudoClass("no"); | ||
| 13 | |||
| 14 | @FXML private Label numLabel; | ||
| 15 | @FXML private Label userLabel; | ||
| 16 | @FXML private Label optionLabel; | ||
| 17 | @FXML private Label sharesLabel; | ||
| 18 | @FXML private Label paidLabel; | ||
| 19 | |||
| 20 | public TradeHistoryItemView() { | ||
| 21 | Views.loadRoot(this, "trade_history_item.fxml"); | ||
| 22 | } | ||
| 23 | |||
| 24 | public TradeHistoryItemView(TradeRow t) { | ||
| 25 | this(); | ||
| 26 | update(t); | ||
| 27 | } | ||
| 28 | |||
| 29 | public void update(TradeRow t) { | ||
| 30 | numLabel.setText("#" + t.n()); | ||
| 31 | userLabel.setText(t.user()); | ||
| 32 | optionLabel.setText(t.option()); | ||
| 33 | optionLabel.pseudoClassStateChanged(YES_STATE, t.yesOption()); | ||
| 34 | optionLabel.pseudoClassStateChanged(NO_STATE, !t.yesOption()); | ||
| 35 | sharesLabel.setText(t.shares() + " shares"); | ||
| 36 | paidLabel.setText(t.paid()); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java new file mode 100644 index 0000000..bf4a5a0 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/TradeSide.java | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | public enum TradeSide { | ||
| 4 | BUY, | ||
| 5 | SELL | ||
| 6 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java new file mode 100644 index 0000000..0e1dea7 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserEventItemView.java | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.css.PseudoClass; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.HBox; | ||
| 7 | import market.guess.ui.desktop.model.UserEventRow; | ||
| 8 | import market.guess.ui.desktop.util.Views; | ||
| 9 | |||
| 10 | public class UserEventItemView extends HBox { | ||
| 11 | private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative"); | ||
| 12 | |||
| 13 | @FXML private Label nameLabel; | ||
| 14 | @FXML private Label roleLabel; | ||
| 15 | @FXML private Label typeLabel; | ||
| 16 | @FXML private Label yesLabel; | ||
| 17 | @FXML private Label noLabel; | ||
| 18 | @FXML private Label plLabel; | ||
| 19 | |||
| 20 | public UserEventItemView() { | ||
| 21 | Views.loadRoot(this, "user_event_item.fxml"); | ||
| 22 | } | ||
| 23 | |||
| 24 | public UserEventItemView(UserEventRow ev) { | ||
| 25 | this(); | ||
| 26 | update(ev); | ||
| 27 | } | ||
| 28 | |||
| 29 | public void update(UserEventRow ev) { | ||
| 30 | nameLabel.setText(ev.eventName()); | ||
| 31 | roleLabel.setText(ev.role()); | ||
| 32 | typeLabel.setText(ev.type()); | ||
| 33 | yesLabel.setText(String.valueOf(ev.yes())); | ||
| 34 | noLabel.setText(String.valueOf(ev.no())); | ||
| 35 | plLabel.setText(ev.pl()); | ||
| 36 | plLabel.pseudoClassStateChanged(NEGATIVE, ev.pl().startsWith("-")); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java new file mode 100644 index 0000000..390cc77 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListCell.java | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.scene.control.ContentDisplay; | ||
| 4 | import javafx.scene.control.ListCell; | ||
| 5 | import market.guess.ui.desktop.model.UserData; | ||
| 6 | |||
| 7 | public class UserListCell extends ListCell<UserData> { | ||
| 8 | private final UserListItemView view = new UserListItemView(); | ||
| 9 | |||
| 10 | public UserListCell() { | ||
| 11 | setContentDisplay(ContentDisplay.GRAPHIC_ONLY); | ||
| 12 | selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { | ||
| 13 | if (getItem() != null) { | ||
| 14 | view.update(getItem(), isNowSelected); | ||
| 15 | } | ||
| 16 | }); | ||
| 17 | } | ||
| 18 | |||
| 19 | @Override | ||
| 20 | protected void updateItem(UserData item, boolean empty) { | ||
| 21 | super.updateItem(item, empty); | ||
| 22 | if (empty || item == null) { | ||
| 23 | setGraphic(null); | ||
| 24 | } else { | ||
| 25 | view.update(item, isSelected()); | ||
| 26 | setGraphic(view); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java new file mode 100644 index 0000000..eb6e7a0 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/UserListItemView.java | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | package market.guess.ui.desktop.components; | ||
| 2 | |||
| 3 | import javafx.css.PseudoClass; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Label; | ||
| 6 | import javafx.scene.layout.HBox; | ||
| 7 | import market.guess.ui.desktop.model.UserData; | ||
| 8 | import market.guess.ui.desktop.util.Format; | ||
| 9 | import market.guess.ui.desktop.util.Views; | ||
| 10 | |||
| 11 | public class UserListItemView extends HBox { | ||
| 12 | private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative"); | ||
| 13 | |||
| 14 | @FXML private Label nameLabel; | ||
| 15 | @FXML private Label roleLabel; | ||
| 16 | @FXML private Label balanceLabel; | ||
| 17 | |||
| 18 | public UserListItemView() { | ||
| 19 | Views.loadRoot(this, "user_list_item.fxml"); | ||
| 20 | } | ||
| 21 | |||
| 22 | public UserListItemView(UserData u, boolean selected) { | ||
| 23 | this(); | ||
| 24 | update(u, selected); | ||
| 25 | } | ||
| 26 | |||
| 27 | public void update(UserData u, boolean selected) { | ||
| 28 | nameLabel.setText(u.name); | ||
| 29 | roleLabel.setText(u.role); | ||
| 30 | balanceLabel.setText(Format.money(u.balance)); | ||
| 31 | balanceLabel.pseudoClassStateChanged(NEGATIVE, u.balance.signum() < 0); | ||
| 32 | Views.setSelected(this, selected); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java new file mode 100644 index 0000000..77059a9 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/AnimationToggleGraphic.java | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.box; | ||
| 4 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled; | ||
| 5 | |||
| 6 | import java.util.function.DoubleConsumer; | ||
| 7 | import javafx.scene.Group; | ||
| 8 | import javafx.scene.shape.Circle; | ||
| 9 | import javafx.scene.shape.SVGPath; | ||
| 10 | import javafx.util.Duration; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Spinny graphic for the animation toggle button. | ||
| 14 | * Renders a kinetic 4-blade turbine / pinwheel that spins continuously when active, | ||
| 15 | * and rests stationary at 0 degrees when inactive. | ||
| 16 | */ | ||
| 17 | public final class AnimationToggleGraphic { | ||
| 18 | private static final Duration CYCLE = Duration.seconds(2.4); | ||
| 19 | |||
| 20 | private static final String BLADES_PATH = | ||
| 21 | "M 10 10 L 10 2 A 8 8 0 0 1 15.65 4.35 Q 12 8 10 10 Z " | ||
| 22 | + "M 10 10 L 18 10 A 8 8 0 0 1 15.65 15.65 Q 12 12 10 10 Z " | ||
| 23 | + "M 10 10 L 10 18 A 8 8 0 0 1 4.35 15.65 Q 8 12 10 10 Z " | ||
| 24 | + "M 10 10 L 2 10 A 8 8 0 0 1 4.35 4.35 Q 8 8 10 10 Z"; | ||
| 25 | |||
| 26 | private AnimationToggleGraphic() {} | ||
| 27 | |||
| 28 | public static Graphic create() { | ||
| 29 | var blades = styled(new SVGPath(), "gm-anim-spinner"); | ||
| 30 | blades.setContent(BLADES_PATH); | ||
| 31 | |||
| 32 | var hub = styled(new Circle(10, 10, 2.2), "gm-anim-spinner-hub"); | ||
| 33 | |||
| 34 | var art = new Group(blades, hub); | ||
| 35 | |||
| 36 | DoubleConsumer render = | ||
| 37 | p -> { | ||
| 38 | // Seamless continuous 360-degree rotation when active; rests at 0 when stopped | ||
| 39 | art.setRotate(p >= 1.0 ? 0.0 : p * 360.0); | ||
| 40 | }; | ||
| 41 | |||
| 42 | return new Graphic(box(art, 20, 20, "gm-anim-toggle-graphic"), render, CYCLE); | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java new file mode 100644 index 0000000..16f8735 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/EqualizerGraphic.java | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.at; | ||
| 4 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.box; | ||
| 5 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled; | ||
| 6 | |||
| 7 | import java.util.function.DoubleConsumer; | ||
| 8 | import javafx.scene.Group; | ||
| 9 | import javafx.scene.shape.Rectangle; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Events tab LIVE pill equalizer graphic. | ||
| 13 | */ | ||
| 14 | public final class EqualizerGraphic { | ||
| 15 | public static final double BAR_H = 12; | ||
| 16 | |||
| 17 | private EqualizerGraphic() {} | ||
| 18 | |||
| 19 | public static Graphic create() { | ||
| 20 | var bars = new Rectangle[3]; | ||
| 21 | for (int i = 0; i < bars.length; i++) { | ||
| 22 | var bar = new Rectangle(i * 6, 0, 4, BAR_H); | ||
| 23 | bar.setArcWidth(2); | ||
| 24 | bar.setArcHeight(2); | ||
| 25 | bars[i] = styled(bar, "gm-anim-bar"); | ||
| 26 | } | ||
| 27 | DoubleConsumer render = | ||
| 28 | p -> { | ||
| 29 | // Three offset loops: order flow arriving. The final frame leaves all three legible. | ||
| 30 | bar(bars[0], at(p, 0, .35, .30, 1, .60, .6, 1, .35)); | ||
| 31 | bar(bars[1], at(p, 0, .8, .25, .3, .70, 1, 1, .8)); | ||
| 32 | bar(bars[2], at(p, 0, .55, .45, 1, .80, .4, 1, .55)); | ||
| 33 | }; | ||
| 34 | return new Graphic(box(new Group(bars), 16, BAR_H, "gm-anim-equalizer"), render); | ||
| 35 | } | ||
| 36 | |||
| 37 | /** Grows the bar from a bottom origin — JavaFX scaleY would pivot at the centre. */ | ||
| 38 | private static void bar(Rectangle r, double scale) { | ||
| 39 | r.setHeight(BAR_H * scale); | ||
| 40 | r.setY(BAR_H - BAR_H * scale); | ||
| 41 | } | ||
| 42 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java new file mode 100644 index 0000000..a337928 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Graphic.java | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import java.util.function.DoubleConsumer; | ||
| 4 | import javafx.animation.Animation; | ||
| 5 | import javafx.animation.Interpolator; | ||
| 6 | import javafx.animation.KeyFrame; | ||
| 7 | import javafx.animation.KeyValue; | ||
| 8 | import javafx.animation.Timeline; | ||
| 9 | import javafx.beans.property.DoubleProperty; | ||
| 10 | import javafx.beans.property.SimpleDoubleProperty; | ||
| 11 | import javafx.scene.layout.Pane; | ||
| 12 | import javafx.util.Duration; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Runtime wrapper for an animated decorative graphic driven by a periodic timeline. | ||
| 16 | */ | ||
| 17 | public class Graphic { | ||
| 18 | public static final Duration CYCLE = Duration.seconds(3.6); | ||
| 19 | |||
| 20 | public final Pane node; | ||
| 21 | |||
| 22 | public Pane getNode() { | ||
| 23 | return node; | ||
| 24 | } | ||
| 25 | |||
| 26 | private final DoubleConsumer render; | ||
| 27 | private final DoubleProperty progress = new SimpleDoubleProperty(1); | ||
| 28 | private final Timeline timeline; | ||
| 29 | |||
| 30 | public Graphic(Pane node, DoubleConsumer render) { | ||
| 31 | this(node, render, CYCLE); | ||
| 32 | } | ||
| 33 | |||
| 34 | public Graphic(Pane node, DoubleConsumer render, Duration cycle) { | ||
| 35 | this.node = node; | ||
| 36 | this.render = render; | ||
| 37 | progress.addListener((obs, old, val) -> render.accept(val.doubleValue())); | ||
| 38 | timeline = | ||
| 39 | new Timeline( | ||
| 40 | new KeyFrame(Duration.ZERO, new KeyValue(progress, 0.0, Interpolator.LINEAR)), | ||
| 41 | new KeyFrame(cycle, new KeyValue(progress, 1.0, Interpolator.LINEAR))); | ||
| 42 | timeline.setCycleCount(Animation.INDEFINITE); | ||
| 43 | render.accept(1); | ||
| 44 | } | ||
| 45 | |||
| 46 | public void setPlaying(boolean playing) { | ||
| 47 | if (playing == (timeline.getStatus() == Animation.Status.RUNNING)) { | ||
| 48 | return; | ||
| 49 | } | ||
| 50 | timeline.stop(); | ||
| 51 | if (playing) { | ||
| 52 | timeline.playFromStart(); | ||
| 53 | } else { | ||
| 54 | render.accept(1); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | public Timeline getTimeline() { | ||
| 59 | return timeline; | ||
| 60 | } | ||
| 61 | |||
| 62 | public DoubleProperty progressProperty() { | ||
| 63 | return progress; | ||
| 64 | } | ||
| 65 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java new file mode 100644 index 0000000..1dcf2ba --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/GraphicHelper.java | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import javafx.animation.Interpolator; | ||
| 4 | import javafx.scene.Node; | ||
| 5 | import javafx.scene.layout.Pane; | ||
| 6 | import javafx.scene.shape.Circle; | ||
| 7 | import javafx.scene.shape.Shape; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Shared utility functions for styling shapes, containers, and interpolation math. | ||
| 11 | */ | ||
| 12 | public final class GraphicHelper { | ||
| 13 | private GraphicHelper() {} | ||
| 14 | |||
| 15 | public static <T extends Shape> T styled(T shape, String styleClass) { | ||
| 16 | shape.getStyleClass().add(styleClass); | ||
| 17 | return shape; | ||
| 18 | } | ||
| 19 | |||
| 20 | public static Circle dot(double r, String styleClass) { | ||
| 21 | return styled(new Circle(r), styleClass); | ||
| 22 | } | ||
| 23 | |||
| 24 | public static void place(Circle dot, double[] xy) { | ||
| 25 | dot.setCenterX(xy[0]); | ||
| 26 | dot.setCenterY(xy[1]); | ||
| 27 | } | ||
| 28 | |||
| 29 | public static Pane initBox(Pane pane, Node art, double w, double h, String styleClass) { | ||
| 30 | pane.getChildren().setAll(art); | ||
| 31 | pane.getStyleClass().add(styleClass); | ||
| 32 | pane.setMinSize(w, h); | ||
| 33 | pane.setPrefSize(w, h); | ||
| 34 | pane.setMaxSize(w, h); | ||
| 35 | return pane; | ||
| 36 | } | ||
| 37 | |||
| 38 | public static Pane box(Node art, double w, double h, String styleClass) { | ||
| 39 | return initBox(new Pane(), art, w, h, styleClass); | ||
| 40 | } | ||
| 41 | |||
| 42 | public static double span(double p, double from, double to) { | ||
| 43 | return Math.max(0, Math.min(1, (p - from) / (to - from))); | ||
| 44 | } | ||
| 45 | |||
| 46 | public static double eased(double t) { | ||
| 47 | return Interpolator.EASE_BOTH.interpolate(0.0, 1.0, t); | ||
| 48 | } | ||
| 49 | |||
| 50 | public static double wrap(double p) { | ||
| 51 | return p < 0 ? p + 1 : p; | ||
| 52 | } | ||
| 53 | |||
| 54 | public static double at(double p, double... stops) { | ||
| 55 | for (int i = 2; i < stops.length; i += 2) { | ||
| 56 | if (p <= stops[i]) { | ||
| 57 | double width = stops[i] - stops[i - 2]; | ||
| 58 | double t = width <= 0 ? 1 : (p - stops[i - 2]) / width; | ||
| 59 | return stops[i - 1] + t * (stops[i + 1] - stops[i - 1]); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | return stops[stops.length - 1]; | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java new file mode 100644 index 0000000..9de978c --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChart.java | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import javafx.scene.layout.Pane; | ||
| 4 | |||
| 5 | /** | ||
| 6 | * Custom declarative JavaFX component for the empty-state decorative animated market chart. | ||
| 7 | * | ||
| 8 | * <p>Can be declared directly in FXML: | ||
| 9 | * | ||
| 10 | * <pre>{@code | ||
| 11 | * <MarketChart fx:id="marketChart" /> | ||
| 12 | * }</pre> | ||
| 13 | */ | ||
| 14 | public class MarketChart extends Pane { | ||
| 15 | private final Graphic graphic; | ||
| 16 | |||
| 17 | public MarketChart() { | ||
| 18 | this.graphic = MarketChartGraphic.build(this); | ||
| 19 | } | ||
| 20 | |||
| 21 | public void setPlaying(boolean playing) { | ||
| 22 | graphic.setPlaying(playing); | ||
| 23 | } | ||
| 24 | |||
| 25 | public Graphic getGraphic() { | ||
| 26 | return graphic; | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java new file mode 100644 index 0000000..1626f25 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/MarketChartGraphic.java | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.at; | ||
| 4 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.dot; | ||
| 5 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.eased; | ||
| 6 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.initBox; | ||
| 7 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.place; | ||
| 8 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.span; | ||
| 9 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled; | ||
| 10 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.wrap; | ||
| 11 | |||
| 12 | import java.util.function.DoubleConsumer; | ||
| 13 | import javafx.scene.Group; | ||
| 14 | import javafx.scene.layout.Pane; | ||
| 15 | import javafx.scene.shape.CubicCurveTo; | ||
| 16 | import javafx.scene.shape.Line; | ||
| 17 | import javafx.scene.shape.MoveTo; | ||
| 18 | import javafx.scene.shape.Path; | ||
| 19 | import javafx.scene.shape.StrokeLineCap; | ||
| 20 | import javafx.scene.text.Text; | ||
| 21 | import javafx.scene.transform.Scale; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Empty-state market chart decorative graphic. | ||
| 25 | * Authored in 240x130 viewBox, then scaled to 300x162 display size. | ||
| 26 | */ | ||
| 27 | public final class MarketChartGraphic { | ||
| 28 | public static final double VIEW_SCALE = 1.25; | ||
| 29 | |||
| 30 | /** Cubic control net: p0, c1, c2, p3. Both curves leave the 0.50 midline together. */ | ||
| 31 | public static final double[] YES_CURVE = {22, 66, 96, 64, 150, 22, 224, 22}; | ||
| 32 | public static final double[] NO_CURVE = {22, 66, 96, 68, 150, 110, 224, 110}; | ||
| 33 | |||
| 34 | private MarketChartGraphic() {} | ||
| 35 | |||
| 36 | public static Graphic create() { | ||
| 37 | return build(new Pane()); | ||
| 38 | } | ||
| 39 | |||
| 40 | public static Graphic build(Pane pane) { | ||
| 41 | var yes = curve(YES_CURVE, "gm-anim-curve-yes"); | ||
| 42 | var no = curve(NO_CURVE, "gm-anim-curve-no"); | ||
| 43 | var yesDot = dot(4, "gm-anim-dot-yes"); | ||
| 44 | var noDot = dot(4, "gm-anim-dot-no"); | ||
| 45 | var tickHi = tickLabel("1.0", 24); | ||
| 46 | var tickLo = tickLabel("0.0", 114); | ||
| 47 | |||
| 48 | var art = | ||
| 49 | new Group( | ||
| 50 | rule(20, false), | ||
| 51 | rule(65, true), | ||
| 52 | rule(110, false), | ||
| 53 | styled(new Line(18, 14, 18, 116), "gm-anim-rule"), | ||
| 54 | tickHi, | ||
| 55 | tickLo, | ||
| 56 | yes, | ||
| 57 | no, | ||
| 58 | yesDot, | ||
| 59 | noDot); | ||
| 60 | art.getTransforms().add(new Scale(VIEW_SCALE, VIEW_SCALE)); | ||
| 61 | |||
| 62 | DoubleConsumer render = | ||
| 63 | p -> { | ||
| 64 | double draw = eased(span(p, 0, .55)); | ||
| 65 | yes.setStrokeDashOffset(300 * (1 - draw)); | ||
| 66 | no.setStrokeDashOffset(300 * (1 - draw)); | ||
| 67 | place(yesDot, rideCurve(YES_CURVE, draw)); | ||
| 68 | place(noDot, rideCurve(NO_CURVE, draw)); | ||
| 69 | double dotFade = at(p, 0, 0, .08, 1); | ||
| 70 | yesDot.setOpacity(dotFade); | ||
| 71 | noDot.setOpacity(dotFade); | ||
| 72 | tickHi.setOpacity(tickOpacity(p)); | ||
| 73 | tickLo.setOpacity(tickOpacity(wrap(p - .11))); | ||
| 74 | }; | ||
| 75 | initBox(pane, art, 300, 162, "gm-anim-market-chart"); | ||
| 76 | return new Graphic(pane, render); | ||
| 77 | } | ||
| 78 | |||
| 79 | private static Path curve(double[] c, String styleClass) { | ||
| 80 | var path = | ||
| 81 | new Path(new MoveTo(c[0], c[1]), new CubicCurveTo(c[2], c[3], c[4], c[5], c[6], c[7])); | ||
| 82 | path.setFill(null); | ||
| 83 | path.setStrokeWidth(2.5); | ||
| 84 | path.setStrokeLineCap(StrokeLineCap.ROUND); | ||
| 85 | // Curve length is ~210, so a 300 dash covers the whole path with room to spare. | ||
| 86 | path.getStrokeDashArray().setAll(300.0, 300.0); | ||
| 87 | return styled(path, styleClass); | ||
| 88 | } | ||
| 89 | |||
| 90 | /** | ||
| 91 | * The cubic's {x, y} at parameter {@code t}. | ||
| 92 | */ | ||
| 93 | public static double[] rideCurve(double[] c, double t) { | ||
| 94 | double u = 1 - t; | ||
| 95 | double b0 = u * u * u; | ||
| 96 | double b1 = 3 * u * u * t; | ||
| 97 | double b2 = 3 * u * t * t; | ||
| 98 | double b3 = t * t * t; | ||
| 99 | return new double[] { | ||
| 100 | b0 * c[0] + b1 * c[2] + b2 * c[4] + b3 * c[6], b0 * c[1] + b1 * c[3] + b2 * c[5] + b3 * c[7] | ||
| 101 | }; | ||
| 102 | } | ||
| 103 | |||
| 104 | private static Line rule(double y, boolean dashed) { | ||
| 105 | var line = new Line(18, y, 228, y); | ||
| 106 | if (dashed) { | ||
| 107 | line.getStrokeDashArray().setAll(3.0, 4.0); | ||
| 108 | } | ||
| 109 | return styled(line, "gm-anim-rule"); | ||
| 110 | } | ||
| 111 | |||
| 112 | private static Text tickLabel(String s, double baseline) { | ||
| 113 | var text = new Text(0, baseline, s); | ||
| 114 | return styled(text, "gm-anim-tick"); | ||
| 115 | } | ||
| 116 | |||
| 117 | /** Axis labels breathe between 25% and 70% opacity. */ | ||
| 118 | private static double tickOpacity(double p) { | ||
| 119 | return .25 + .45 * (1 - Math.abs(2 * p - 1)); | ||
| 120 | } | ||
| 121 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java new file mode 100644 index 0000000..4e8b6f2 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/Motion.java | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import javafx.scene.layout.Pane; | ||
| 4 | import javafx.util.Duration; | ||
| 5 | |||
| 6 | /** | ||
| 7 | * Coordinating entry point and facade for decorative animated graphics. | ||
| 8 | */ | ||
| 9 | public final class Motion { | ||
| 10 | public static final Duration CYCLE = Graphic.CYCLE; | ||
| 11 | |||
| 12 | public static final double[] YES_CURVE = MarketChartGraphic.YES_CURVE; | ||
| 13 | public static final double[] NO_CURVE = MarketChartGraphic.NO_CURVE; | ||
| 14 | public static final double[] SPARK = SparklineGraphic.SPARK; | ||
| 15 | |||
| 16 | private Motion() {} | ||
| 17 | |||
| 18 | // ---- Market Chart ------------------------------------------------------ | ||
| 19 | |||
| 20 | public static Graphic marketChart() { | ||
| 21 | return MarketChartGraphic.create(); | ||
| 22 | } | ||
| 23 | |||
| 24 | public static Graphic buildMarketChart(Pane pane) { | ||
| 25 | return MarketChartGraphic.build(pane); | ||
| 26 | } | ||
| 27 | |||
| 28 | public static double[] rideCurve(double[] c, double t) { | ||
| 29 | return MarketChartGraphic.rideCurve(c, t); | ||
| 30 | } | ||
| 31 | |||
| 32 | // ---- Equalizer --------------------------------------------------------- | ||
| 33 | |||
| 34 | public static Graphic equalizer() { | ||
| 35 | return EqualizerGraphic.create(); | ||
| 36 | } | ||
| 37 | |||
| 38 | // ---- Sparkline --------------------------------------------------------- | ||
| 39 | |||
| 40 | public static Graphic sparkline() { | ||
| 41 | return SparklineGraphic.create(); | ||
| 42 | } | ||
| 43 | |||
| 44 | public static double[] ridePolyline(double[] pts, double t) { | ||
| 45 | return SparklineGraphic.ridePolyline(pts, t); | ||
| 46 | } | ||
| 47 | |||
| 48 | // ---- Animation Toggle -------------------------------------------------- | ||
| 49 | |||
| 50 | public static Graphic animationToggle() { | ||
| 51 | return AnimationToggleGraphic.create(); | ||
| 52 | } | ||
| 53 | |||
| 54 | // ---- Interpolation ----------------------------------------------------- | ||
| 55 | |||
| 56 | public static double at(double p, double... stops) { | ||
| 57 | return GraphicHelper.at(p, stops); | ||
| 58 | } | ||
| 59 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java new file mode 100644 index 0000000..8816b54 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/components/graphic/SparklineGraphic.java | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | package market.guess.ui.desktop.components.graphic; | ||
| 2 | |||
| 3 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.at; | ||
| 4 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.box; | ||
| 5 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.dot; | ||
| 6 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.eased; | ||
| 7 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.place; | ||
| 8 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.span; | ||
| 9 | import static market.guess.ui.desktop.components.graphic.GraphicHelper.styled; | ||
| 10 | |||
| 11 | import java.util.function.DoubleConsumer; | ||
| 12 | import javafx.scene.Group; | ||
| 13 | import javafx.scene.shape.Polyline; | ||
| 14 | import javafx.scene.shape.StrokeLineCap; | ||
| 15 | import javafx.scene.shape.StrokeLineJoin; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Users tab balance sparkline graphic. | ||
| 19 | */ | ||
| 20 | public final class SparklineGraphic { | ||
| 21 | public static final double[] SPARK = {2, 20, 14, 16, 26, 22, 38, 11, 50, 14, 62, 5}; | ||
| 22 | |||
| 23 | private SparklineGraphic() {} | ||
| 24 | |||
| 25 | public static Graphic create() { | ||
| 26 | var line = new Polyline(SPARK); | ||
| 27 | line.setFill(null); | ||
| 28 | line.setStrokeWidth(2); | ||
| 29 | line.setStrokeLineCap(StrokeLineCap.ROUND); | ||
| 30 | line.setStrokeLineJoin(StrokeLineJoin.ROUND); | ||
| 31 | line.getStrokeDashArray().setAll(120.0, 120.0); | ||
| 32 | styled(line, "gm-anim-spark"); | ||
| 33 | |||
| 34 | var dot = dot(2.8, "gm-anim-spark-dot"); | ||
| 35 | |||
| 36 | DoubleConsumer render = | ||
| 37 | p -> { | ||
| 38 | double draw = eased(span(p, 0, .70)); | ||
| 39 | line.setStrokeDashOffset(120 * (1 - draw)); | ||
| 40 | dot.setOpacity(at(p, 0, 0, .07, 1)); | ||
| 41 | place(dot, ridePolyline(SPARK, draw)); | ||
| 42 | }; | ||
| 43 | return new Graphic(box(new Group(line, dot), 64, 28, "gm-anim-sparkline"), render); | ||
| 44 | } | ||
| 45 | |||
| 46 | /** The polyline's {x, y} at arc-length fraction {@code t}, so the dot sits on the drawn tip. */ | ||
| 47 | public static double[] ridePolyline(double[] pts, double t) { | ||
| 48 | double total = 0; | ||
| 49 | for (int i = 2; i < pts.length; i += 2) { | ||
| 50 | total += Math.hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1]); | ||
| 51 | } | ||
| 52 | double target = total * t; | ||
| 53 | for (int i = 2; i < pts.length; i += 2) { | ||
| 54 | double seg = Math.hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1]); | ||
| 55 | if (target <= seg || i == pts.length - 2) { | ||
| 56 | double f = seg <= 0 ? 1 : Math.min(target / seg, 1); | ||
| 57 | return new double[] { | ||
| 58 | pts[i - 2] + f * (pts[i] - pts[i - 2]), pts[i - 1] + f * (pts[i + 1] - pts[i - 1]) | ||
| 59 | }; | ||
| 60 | } | ||
| 61 | target -= seg; | ||
| 62 | } | ||
| 63 | return new double[] {pts[0], pts[1]}; | ||
| 64 | } | ||
| 65 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java new file mode 100644 index 0000000..c582051 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/AppController.java | |||
| @@ -0,0 +1,592 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.nio.file.Paths; | ||
| 5 | import javafx.animation.FadeTransition; | ||
| 6 | import javafx.animation.PauseTransition; | ||
| 7 | import javafx.beans.binding.Bindings; | ||
| 8 | import javafx.collections.ListChangeListener; | ||
| 9 | import javafx.css.PseudoClass; | ||
| 10 | import javafx.fxml.FXML; | ||
| 11 | import javafx.scene.Node; | ||
| 12 | import javafx.scene.control.Button; | ||
| 13 | import javafx.scene.control.ComboBox; | ||
| 14 | import javafx.scene.control.Label; | ||
| 15 | import javafx.scene.control.TabPane; | ||
| 16 | import javafx.scene.layout.BorderPane; | ||
| 17 | import javafx.scene.layout.HBox; | ||
| 18 | import javafx.scene.layout.Region; | ||
| 19 | import javafx.scene.layout.StackPane; | ||
| 20 | import javafx.stage.FileChooser; | ||
| 21 | import javafx.stage.FileChooser.ExtensionFilter; | ||
| 22 | import javafx.util.Duration; | ||
| 23 | import market.guess.model.event.CreateEventRequest; | ||
| 24 | import market.guess.model.event.EventStatus; | ||
| 25 | import market.guess.model.event.MechanismType; | ||
| 26 | import market.guess.ui.desktop.AppState; | ||
| 27 | import market.guess.ui.desktop.AppView; | ||
| 28 | import market.guess.ui.desktop.Skin; | ||
| 29 | import market.guess.ui.desktop.components.AppTab; | ||
| 30 | import market.guess.ui.desktop.components.ModalDialog; | ||
| 31 | import market.guess.ui.desktop.components.graphic.AnimationToggleGraphic; | ||
| 32 | import market.guess.ui.desktop.components.graphic.Graphic; | ||
| 33 | import market.guess.ui.desktop.model.EventData; | ||
| 34 | import market.guess.ui.desktop.model.UserData; | ||
| 35 | import market.guess.ui.desktop.task.InitialLoadTask; | ||
| 36 | import market.guess.ui.desktop.util.Format; | ||
| 37 | import market.guess.ui.desktop.util.Views; | ||
| 38 | |||
| 39 | public class AppController { | ||
| 40 | private static final PseudoClass SKIN_ROSE_PINE = | ||
| 41 | PseudoClass.getPseudoClass("skin-rose-pine-dawn"); | ||
| 42 | private static final PseudoClass SKIN_CATPPUCCIN = PseudoClass.getPseudoClass("skin-catppuccin"); | ||
| 43 | private static final PseudoClass SKIN_GRUVBOX = PseudoClass.getPseudoClass("skin-gruvbox"); | ||
| 44 | |||
| 45 | private static final Duration TOAST_DURATION = Duration.millis(2500); | ||
| 46 | private static final Duration FADE_AWAY_DURATION = Duration.millis(500); | ||
| 47 | private AppView appView; | ||
| 48 | private double dragOffsetX; | ||
| 49 | private double dragOffsetY; | ||
| 50 | private InitialLoadTask currentLoadTask; | ||
| 51 | private Graphic animationsGraphic; | ||
| 52 | |||
| 53 | @FXML private StackPane rootStack; | ||
| 54 | @FXML private BorderPane chrome; | ||
| 55 | @FXML private HBox windowBar; | ||
| 56 | @FXML private TabPane tabPane; | ||
| 57 | @FXML private HBox tabInfo; | ||
| 58 | @FXML private Label loadedFilePathLabel; | ||
| 59 | @FXML private ComboBox<String> skinComboBox; | ||
| 60 | @FXML private Button animationsButton; | ||
| 61 | @FXML private Label balanceValueLabel; | ||
| 62 | @FXML private Label actingAsLabel; | ||
| 63 | @FXML private StackPane centerContainer; | ||
| 64 | @FXML private Label statusBarLabel; | ||
| 65 | @FXML private Button ejectButton; | ||
| 66 | @FXML private StackPane dialogContainer; | ||
| 67 | @FXML private StackPane toastContainer; | ||
| 68 | @FXML private Label toastLabel; | ||
| 69 | |||
| 70 | // Injected by FXMLLoader from app_view.fxml via <fx:include> | ||
| 71 | @FXML private Node emptyState; | ||
| 72 | @FXML private EmptyStateController emptyStateController; | ||
| 73 | @FXML private Node eventsTab; | ||
| 74 | @FXML private EventsTabController eventsTabController; | ||
| 75 | @FXML private Node usersTab; | ||
| 76 | @FXML private UsersTabController usersTabController; | ||
| 77 | @FXML private Node loadDialog; | ||
| 78 | @FXML private LoadDialogController loadDialogController; | ||
| 79 | @FXML private Node createEventDialog; | ||
| 80 | @FXML private CreateEventDialogController createEventDialogController; | ||
| 81 | @FXML private Node resolveDialog; | ||
| 82 | @FXML private ResolveDialogController resolveDialogController; | ||
| 83 | |||
| 84 | private Node currentlyActiveContent; | ||
| 85 | |||
| 86 | @FXML | ||
| 87 | private void initialize() { | ||
| 88 | loadDialogController.setOnCancel(this::cancelLoad); | ||
| 89 | createEventDialogController.setOnCancel(this::closeDialog); | ||
| 90 | createEventDialogController.setOnCreate((CreateEventRequest req) -> createEvent(req)); | ||
| 91 | resolveDialogController.setOnCancel(this::closeDialog); | ||
| 92 | resolveDialogController.setOnConfirm(this::resolveEvent); | ||
| 93 | } | ||
| 94 | |||
| 95 | public void init(AppView appView) { | ||
| 96 | this.appView = appView; | ||
| 97 | setupWindowDrag(); | ||
| 98 | setupSkinComboBox(); | ||
| 99 | setupAnimationsButton(); | ||
| 100 | initSubViews(); | ||
| 101 | setupStateBindings(); | ||
| 102 | applyInitialState(); | ||
| 103 | } | ||
| 104 | |||
| 105 | private void setupAnimationsButton() { | ||
| 106 | animationsGraphic = AnimationToggleGraphic.create(); | ||
| 107 | if (animationsButton != null) { | ||
| 108 | animationsButton.setGraphic(animationsGraphic.getNode()); | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | private void setupWindowDrag() { | ||
| 113 | windowBar.setOnMousePressed( | ||
| 114 | e -> { | ||
| 115 | dragOffsetX = e.getSceneX(); | ||
| 116 | dragOffsetY = e.getSceneY(); | ||
| 117 | }); | ||
| 118 | windowBar.setOnMouseDragged( | ||
| 119 | e -> { | ||
| 120 | appView.getStage().setX(e.getScreenX() - dragOffsetX); | ||
| 121 | appView.getStage().setY(e.getScreenY() - dragOffsetY); | ||
| 122 | }); | ||
| 123 | windowBar.setOnMouseClicked( | ||
| 124 | e -> { | ||
| 125 | if (e.getClickCount() == 2) { | ||
| 126 | var stage = appView.getStage(); | ||
| 127 | stage.setMaximized(!stage.isMaximized()); | ||
| 128 | } | ||
| 129 | }); | ||
| 130 | } | ||
| 131 | |||
| 132 | private void setupSkinComboBox() { | ||
| 133 | skinComboBox.getItems().setAll("Rosé Pine Dawn", "Catppuccin", "Gruvbox"); | ||
| 134 | skinComboBox.setValue("Rosé Pine Dawn"); | ||
| 135 | } | ||
| 136 | |||
| 137 | private void initSubViews() { | ||
| 138 | var state = getState(); | ||
| 139 | eventsTabController.init(state); | ||
| 140 | eventsTabController.setOnNewEvent(this::openCreateEventDialog); | ||
| 141 | eventsTabController.setOnOpenEvent(this::openEvent); | ||
| 142 | eventsTabController.setOnCloseEvent(this::openResolveDialog); | ||
| 143 | |||
| 144 | usersTabController.init(state); | ||
| 145 | usersTabController.setOnCreateNewEvent(this::openCreateEventDialog); | ||
| 146 | usersTabController.setOnToast(this::toast); | ||
| 147 | usersTabController.setOnPlaceOrder(this::placeOrder); | ||
| 148 | } | ||
| 149 | |||
| 150 | private void setupStateBindings() { | ||
| 151 | var state = appView.getState(); | ||
| 152 | |||
| 153 | // Declarative property bindings | ||
| 154 | loadedFilePathLabel | ||
| 155 | .textProperty() | ||
| 156 | .bind( | ||
| 157 | Bindings.when( | ||
| 158 | state.loadedFileProperty().isNull().or(state.loadedFileProperty().isEmpty())) | ||
| 159 | .then("") | ||
| 160 | .otherwise(state.loadedFileProperty())); | ||
| 161 | |||
| 162 | ejectButton.visibleProperty().bind(state.loadedFileProperty().isNotNull()); | ||
| 163 | ejectButton.managedProperty().bind(ejectButton.visibleProperty()); | ||
| 164 | |||
| 165 | tabInfo.visibleProperty().bind(tabPane.visibleProperty()); | ||
| 166 | // The header area only exists once the skin is installed; match its height so the | ||
| 167 | // overlaid acting-as/balance labels stay vertically centred in it. | ||
| 168 | tabPane | ||
| 169 | .skinProperty() | ||
| 170 | .addListener( | ||
| 171 | (obs, oldV, newV) -> { | ||
| 172 | var header = (Region) tabPane.lookup(".tab-header-area"); | ||
| 173 | tabInfo.prefHeightProperty().bind(header.heightProperty()); | ||
| 174 | }); | ||
| 175 | tabPane | ||
| 176 | .getSelectionModel() | ||
| 177 | .selectedIndexProperty() | ||
| 178 | .addListener((obs, oldV, newV) -> state.setActiveTab(AppTab.values()[newV.intValue()])); | ||
| 179 | |||
| 180 | // Granular reactive listeners | ||
| 181 | state.skinProperty().addListener((obs, oldV, newV) -> updateSkin(newV)); | ||
| 182 | state | ||
| 183 | .loadedFileProperty() | ||
| 184 | .addListener( | ||
| 185 | (obs, oldV, newV) -> { | ||
| 186 | updateCenterContent(); | ||
| 187 | updateStatusBar(); | ||
| 188 | }); | ||
| 189 | state.activeTabProperty().addListener((obs, oldV, newV) -> updateTabs(newV)); | ||
| 190 | state.dialogProperty().addListener((obs, oldV, newV) -> updateDialogOverlay()); | ||
| 191 | state.toastProperty().addListener((obs, oldV, newV) -> updateToastOverlay()); | ||
| 192 | state.animationsOnProperty().addListener((obs, oldV, newV) -> updateAnimations(newV)); | ||
| 193 | state.actingUserProperty().addListener((obs, oldV, newV) -> updateActingUserDisplay()); | ||
| 194 | state.actingUserNameProperty().addListener((obs, oldV, newV) -> updateActingUserDisplay()); | ||
| 195 | |||
| 196 | state.getEvents().addListener((ListChangeListener<EventData>) c -> updateStatusBar()); | ||
| 197 | state.getUsers().addListener((ListChangeListener<UserData>) c -> updateStatusBar()); | ||
| 198 | } | ||
| 199 | |||
| 200 | public AppState getState() { | ||
| 201 | return appView != null ? appView.getState() : null; | ||
| 202 | } | ||
| 203 | |||
| 204 | /** | ||
| 205 | * Refreshes UI state across all components. Kept for lifecycle compatibility; delegates to | ||
| 206 | * targeted update methods. | ||
| 207 | */ | ||
| 208 | public void refresh() { | ||
| 209 | applyInitialState(); | ||
| 210 | } | ||
| 211 | |||
| 212 | private void applyInitialState() { | ||
| 213 | var state = getState(); | ||
| 214 | if (state == null) return; | ||
| 215 | |||
| 216 | updateSkin(state.getSkin()); | ||
| 217 | updateAnimations(state.isAnimationsOn()); | ||
| 218 | updateTabs(state.getActiveTab()); | ||
| 219 | updateActingUserDisplay(); | ||
| 220 | updateCenterContent(); | ||
| 221 | updateStatusBar(); | ||
| 222 | updateDialogOverlay(); | ||
| 223 | updateToastOverlay(); | ||
| 224 | } | ||
| 225 | |||
| 226 | private void updateSkin(Skin skin) { | ||
| 227 | if (skin == null) return; | ||
| 228 | rootStack.pseudoClassStateChanged(SKIN_ROSE_PINE, skin == Skin.ROSE_PINE_DAWN); | ||
| 229 | rootStack.pseudoClassStateChanged(SKIN_CATPPUCCIN, skin == Skin.CATPPUCCIN); | ||
| 230 | rootStack.pseudoClassStateChanged(SKIN_GRUVBOX, skin == Skin.GRUVBOX); | ||
| 231 | |||
| 232 | String comboVal = | ||
| 233 | switch (skin) { | ||
| 234 | case CATPPUCCIN -> "Catppuccin"; | ||
| 235 | case GRUVBOX -> "Gruvbox"; | ||
| 236 | default -> "Rosé Pine Dawn"; | ||
| 237 | }; | ||
| 238 | if (!comboVal.equals(skinComboBox.getValue())) { | ||
| 239 | skinComboBox.setValue(comboVal); | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | private void updateAnimations(boolean animOn) { | ||
| 244 | Views.setSelected(animationsButton, animOn); | ||
| 245 | if (animationsGraphic != null) { | ||
| 246 | animationsGraphic.setPlaying(animOn); | ||
| 247 | } | ||
| 248 | if (emptyStateController != null) { | ||
| 249 | emptyStateController.setAnimating(animOn); | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | private void updateTabs(AppTab tab) { | ||
| 254 | // ponytail: tab order in app_view.fxml must match AppTab declaration order | ||
| 255 | tabPane.getSelectionModel().select(tab.ordinal()); | ||
| 256 | updateCenterContent(); | ||
| 257 | } | ||
| 258 | |||
| 259 | private void updateActingUserDisplay() { | ||
| 260 | var state = getState(); | ||
| 261 | if (state == null) return; | ||
| 262 | var actor = state.getActingUser(); | ||
| 263 | if (actor == null) return; | ||
| 264 | balanceValueLabel.setText(Format.money(actor.balance)); | ||
| 265 | actingAsLabel.setText( | ||
| 266 | "acting as " + (state.getActingUserName() == null ? "" : state.getActingUserName())); | ||
| 267 | } | ||
| 268 | |||
| 269 | private void updateCenterContent() { | ||
| 270 | var state = getState(); | ||
| 271 | if (state == null) return; | ||
| 272 | |||
| 273 | boolean showEmpty = (state.getLoadedFile() == null); | ||
| 274 | boolean showEvents = !showEmpty && (state.getActiveTab() == AppTab.EVENTS); | ||
| 275 | boolean showUsers = !showEmpty && (state.getActiveTab() == AppTab.USERS); | ||
| 276 | |||
| 277 | Node activeContent = showEmpty ? emptyState : (showEvents ? eventsTab : usersTab); | ||
| 278 | |||
| 279 | emptyState.setVisible(showEmpty); | ||
| 280 | emptyState.setManaged(showEmpty); | ||
| 281 | |||
| 282 | tabPane.setVisible(!showEmpty); | ||
| 283 | tabPane.setManaged(!showEmpty); | ||
| 284 | |||
| 285 | if (showEvents && eventsTabController != null) { | ||
| 286 | eventsTabController.refresh(); | ||
| 287 | } | ||
| 288 | if (showUsers && usersTabController != null) { | ||
| 289 | usersTabController.refresh(); | ||
| 290 | } | ||
| 291 | |||
| 292 | if (currentlyActiveContent != activeContent) { | ||
| 293 | Node prev = currentlyActiveContent; | ||
| 294 | currentlyActiveContent = activeContent; | ||
| 295 | if (state.isAnimationsOn() && state.getLoadedFile() != null && prev != null) { | ||
| 296 | var fade = new FadeTransition(FADE_AWAY_DURATION, activeContent); | ||
| 297 | fade.setFromValue(0); | ||
| 298 | fade.setToValue(1); | ||
| 299 | fade.play(); | ||
| 300 | } | ||
| 301 | } | ||
| 302 | } | ||
| 303 | |||
| 304 | private void updateStatusBar() { | ||
| 305 | var state = getState(); | ||
| 306 | if (state == null || statusBarLabel == null) return; | ||
| 307 | |||
| 308 | if (state.getLoadedFile() == null) { | ||
| 309 | statusBarLabel.setText(" IDLE  "); | ||
| 310 | } else { | ||
| 311 | String fileName = Paths.get(state.getLoadedFile()).getFileName().toString(); | ||
| 312 | statusBarLabel.setText( | ||
| 313 | " ï…› " | ||
| 314 | + fileName | ||
| 315 | + "   " | ||
| 316 | + state.getEvents().size() | ||
| 317 | + " events   " | ||
| 318 | + state.getUsers().size() | ||
| 319 | + " users"); | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | private void updateDialogOverlay() { | ||
| 324 | var state = getState(); | ||
| 325 | if (state == null) return; | ||
| 326 | |||
| 327 | boolean showDialog = (state.getDialog() != ModalDialog.NONE); | ||
| 328 | dialogContainer.setVisible(showDialog); | ||
| 329 | dialogContainer.setMouseTransparent(!showDialog); | ||
| 330 | |||
| 331 | boolean isLoad = (state.getDialog() == ModalDialog.LOAD); | ||
| 332 | loadDialog.setVisible(isLoad); | ||
| 333 | loadDialog.setManaged(isLoad); | ||
| 334 | |||
| 335 | boolean isCreate = (state.getDialog() == ModalDialog.CREATE_EVENT); | ||
| 336 | createEventDialog.setVisible(isCreate); | ||
| 337 | createEventDialog.setManaged(isCreate); | ||
| 338 | |||
| 339 | boolean isResolve = (state.getDialog() == ModalDialog.RESOLVE); | ||
| 340 | resolveDialog.setVisible(isResolve); | ||
| 341 | resolveDialog.setManaged(isResolve); | ||
| 342 | } | ||
| 343 | |||
| 344 | private void updateToastOverlay() { | ||
| 345 | var state = getState(); | ||
| 346 | if (state == null) return; | ||
| 347 | |||
| 348 | if (state.getToast() != null) { | ||
| 349 | toastLabel.setText(state.getToast()); | ||
| 350 | toastLabel.setVisible(true); | ||
| 351 | } else { | ||
| 352 | toastLabel.setVisible(false); | ||
| 353 | } | ||
| 354 | } | ||
| 355 | |||
| 356 | // ---- FXML action handlers ---------------------------------------------- | ||
| 357 | |||
| 358 | @FXML | ||
| 359 | private void handleClose() { | ||
| 360 | appView.getStage().close(); | ||
| 361 | } | ||
| 362 | |||
| 363 | @FXML | ||
| 364 | private void handleMinimize() { | ||
| 365 | appView.getStage().setIconified(true); | ||
| 366 | } | ||
| 367 | |||
| 368 | @FXML | ||
| 369 | private void handleMaximize() { | ||
| 370 | var stage = appView.getStage(); | ||
| 371 | stage.setMaximized(!stage.isMaximized()); | ||
| 372 | } | ||
| 373 | |||
| 374 | @FXML | ||
| 375 | private void handleSkinChanged() { | ||
| 376 | String val = skinComboBox.getValue(); | ||
| 377 | if (val == null) return; | ||
| 378 | Skin skin = | ||
| 379 | switch (val) { | ||
| 380 | case "Catppuccin" -> Skin.CATPPUCCIN; | ||
| 381 | case "Gruvbox" -> Skin.GRUVBOX; | ||
| 382 | default -> Skin.ROSE_PINE_DAWN; | ||
| 383 | }; | ||
| 384 | var state = appView.getState(); | ||
| 385 | if (state != null && state.getSkin() != skin) { | ||
| 386 | state.setSkin(skin); | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | @FXML | ||
| 391 | private void handleAnimationsToggled() { | ||
| 392 | appView.getState().setAnimationsOn(!appView.getState().isAnimationsOn()); | ||
| 393 | } | ||
| 394 | |||
| 395 | @FXML | ||
| 396 | private void handleLoadFile() { | ||
| 397 | startLoad(); | ||
| 398 | } | ||
| 399 | |||
| 400 | @FXML | ||
| 401 | private void handleEjectFile() { | ||
| 402 | appView.getState().setLoadedFile(null); | ||
| 403 | } | ||
| 404 | |||
| 405 | // ---- Public Actions --------------------------------------------------- | ||
| 406 | |||
| 407 | public void toast(String msg) { | ||
| 408 | appView.getState().setToast(msg); | ||
| 409 | var pause = new PauseTransition(TOAST_DURATION); | ||
| 410 | pause.setOnFinished(e -> appView.getState().setToast(null)); | ||
| 411 | pause.play(); | ||
| 412 | } | ||
| 413 | |||
| 414 | public void startLoad() { | ||
| 415 | var stage = appView.getStage(); | ||
| 416 | var fc = new FileChooser(); | ||
| 417 | fc.setTitle("Load events file"); | ||
| 418 | fc.getExtensionFilters().add(new ExtensionFilter("GuessMarket Files", "*.xml")); | ||
| 419 | var picked = stage.getScene() == null ? null : fc.showOpenDialog(stage); | ||
| 420 | if (picked == null) { | ||
| 421 | return; | ||
| 422 | } | ||
| 423 | var state = appView.getState(); | ||
| 424 | state.setPendingFile(picked.getAbsolutePath()); | ||
| 425 | |||
| 426 | var loadTask = | ||
| 427 | new InitialLoadTask(appView.getCatalogContext(), Paths.get(state.getPendingFile())); | ||
| 428 | this.currentLoadTask = loadTask; | ||
| 429 | |||
| 430 | loadTask.setOnRunning( | ||
| 431 | e -> { | ||
| 432 | state.setDialog(ModalDialog.LOAD); | ||
| 433 | loadDialogController.show(loadTask, state.getPendingFile()); | ||
| 434 | }); | ||
| 435 | |||
| 436 | loadTask.setOnCancelled( | ||
| 437 | e -> { | ||
| 438 | this.currentLoadTask = null; | ||
| 439 | loadDialogController.reset(); | ||
| 440 | state.setDialog(ModalDialog.NONE); | ||
| 441 | }); | ||
| 442 | |||
| 443 | loadTask.setOnFailed( | ||
| 444 | e -> { | ||
| 445 | this.currentLoadTask = null; | ||
| 446 | loadDialogController.reset(); | ||
| 447 | cancelLoad(); | ||
| 448 | Throwable ex = loadTask.getException(); | ||
| 449 | String msg = | ||
| 450 | (ex != null && ex.getMessage() != null && !ex.getMessage().isBlank()) | ||
| 451 | ? ex.getMessage() | ||
| 452 | : "Failed to load file."; | ||
| 453 | toast(msg); | ||
| 454 | }); | ||
| 455 | |||
| 456 | loadTask.setOnSucceeded( | ||
| 457 | e -> { | ||
| 458 | this.currentLoadTask = null; | ||
| 459 | loadDialogController.reset(); | ||
| 460 | if (state.getDialog() != ModalDialog.LOAD) return; | ||
| 461 | var loadResult = loadTask.getValue(); | ||
| 462 | state.setLoadedFile(loadResult.source()); | ||
| 463 | state.refreshData(); | ||
| 464 | state.setDialog(ModalDialog.NONE); | ||
| 465 | toast( | ||
| 466 | "File loaded  " | ||
| 467 | + loadResult.eventsLoaded() | ||
| 468 | + " events, " | ||
| 469 | + state.getUsers().size() | ||
| 470 | + " users"); | ||
| 471 | }); | ||
| 472 | |||
| 473 | var thread = new Thread(loadTask); | ||
| 474 | thread.setDaemon(true); | ||
| 475 | thread.start(); | ||
| 476 | } | ||
| 477 | |||
| 478 | public void cancelLoad() { | ||
| 479 | if (currentLoadTask != null) { | ||
| 480 | currentLoadTask.cancel(); | ||
| 481 | currentLoadTask = null; | ||
| 482 | } | ||
| 483 | loadDialogController.reset(); | ||
| 484 | appView.getState().setDialog(ModalDialog.NONE); | ||
| 485 | } | ||
| 486 | |||
| 487 | public void openCreateEventDialog() { | ||
| 488 | var state = appView.getState(); | ||
| 489 | state.setCreateType(MechanismType.LMSR); | ||
| 490 | createEventDialogController.reset(); | ||
| 491 | state.setDialog(ModalDialog.CREATE_EVENT); | ||
| 492 | } | ||
| 493 | |||
| 494 | public void closeDialog() { | ||
| 495 | appView.getState().setDialog(ModalDialog.NONE); | ||
| 496 | } | ||
| 497 | |||
| 498 | public void openResolveDialog() { | ||
| 499 | var state = appView.getState(); | ||
| 500 | state.setResolveSelectedOption(null); | ||
| 501 | resolveDialogController.reset(); | ||
| 502 | state.setDialog(ModalDialog.RESOLVE); | ||
| 503 | } | ||
| 504 | |||
| 505 | public void createEvent(CreateEventRequest req) { | ||
| 506 | var state = appView.getState(); | ||
| 507 | state.setCreateType(req.mechanism()); | ||
| 508 | var requestWithMM = | ||
| 509 | new CreateEventRequest( | ||
| 510 | req.name(), | ||
| 511 | req.description(), | ||
| 512 | req.mechanism(), | ||
| 513 | req.commissionPercent(), | ||
| 514 | req.commissionTiming(), | ||
| 515 | req.mmUserName() != null ? req.mmUserName() : state.getActingUserName(), | ||
| 516 | req.liquidityB(), | ||
| 517 | req.baseValueD(), | ||
| 518 | req.options(), | ||
| 519 | req.allowMinting()); | ||
| 520 | var res = appView.getCatalogContext().createEvent(requestWithMM); | ||
| 521 | if (res.isSuccess()) { | ||
| 522 | state.refreshData(); | ||
| 523 | state.setDialog(ModalDialog.NONE); | ||
| 524 | state.setSelectedEventId(res.getData().summary().key()); | ||
| 525 | toast("Event created — you are its market maker"); | ||
| 526 | } else { | ||
| 527 | toast("Failed to create event: " + res.getMessage()); | ||
| 528 | } | ||
| 529 | } | ||
| 530 | |||
| 531 | public void resolveEvent(String option) { | ||
| 532 | var state = appView.getState(); | ||
| 533 | var e = state.getSelectedEvent(); | ||
| 534 | if (e == null) return; | ||
| 535 | var res = appView.getMarketContext().settleEvent(state.getActingUserName(), e.id, option); | ||
| 536 | if (res.isSuccess()) { | ||
| 537 | state.refreshData(); | ||
| 538 | state.setDialog(ModalDialog.NONE); | ||
| 539 | toast("Event resolved as " + option); | ||
| 540 | } else { | ||
| 541 | toast("Failed to resolve event: " + res.getMessage()); | ||
| 542 | } | ||
| 543 | } | ||
| 544 | |||
| 545 | public void openEvent(EventData e) { | ||
| 546 | var state = appView.getState(); | ||
| 547 | if (e.status == EventStatus.DRAFT) { | ||
| 548 | var res = appView.getCatalogContext().openEvent(e.id); | ||
| 549 | if (!res.isSuccess()) { | ||
| 550 | toast("Failed to open event: " + res.getMessage()); | ||
| 551 | return; | ||
| 552 | } | ||
| 553 | state.refreshData(); | ||
| 554 | } | ||
| 555 | // The trade panel lives on the Users tab and follows the selected event. | ||
| 556 | state.setSelectedEventId(e.id); | ||
| 557 | state.setActiveTab(AppTab.USERS); | ||
| 558 | toast("Trading " + e.name + " as " + state.getActingUserName()); | ||
| 559 | } | ||
| 560 | |||
| 561 | public void placeOrder(String eventKey, String optionKey, BigDecimal price, long quantity) { | ||
| 562 | placeOrder(eventKey, optionKey, "BUY", price, quantity); | ||
| 563 | } | ||
| 564 | |||
| 565 | public void placeOrder( | ||
| 566 | String eventKey, String optionKey, String side, BigDecimal price, long quantity) { | ||
| 567 | var state = appView.getState(); | ||
| 568 | var res = | ||
| 569 | appView | ||
| 570 | .getMarketContext() | ||
| 571 | .placeOrder(state.getActingUserName(), eventKey, optionKey, side, price, quantity); | ||
| 572 | if (res.isSuccess()) { | ||
| 573 | state.refreshData(); | ||
| 574 | var receipt = res.getData(); | ||
| 575 | boolean isBuy = "BUY".equalsIgnoreCase(side); | ||
| 576 | if (receipt.trades() == null || receipt.trades().isEmpty()) { | ||
| 577 | toast("Resting " + side + " order placed: " + quantity + " @ " + Format.money(price)); | ||
| 578 | } else { | ||
| 579 | toast( | ||
| 580 | "Order placed  " | ||
| 581 | + (isBuy ? "Paid " : "Received ") | ||
| 582 | + Format.money(new BigDecimal(receipt.totalPaid())) | ||
| 583 | + " for " | ||
| 584 | + quantity | ||
| 585 | + " " | ||
| 586 | + optionKey); | ||
| 587 | } | ||
| 588 | } else { | ||
| 589 | toast("Trade failed: " + res.getMessage()); | ||
| 590 | } | ||
| 591 | } | ||
| 592 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java new file mode 100644 index 0000000..5649145 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/CreateEventDialogController.java | |||
| @@ -0,0 +1,163 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.util.List; | ||
| 5 | import java.util.function.BiConsumer; | ||
| 6 | import java.util.function.Consumer; | ||
| 7 | import javafx.fxml.FXML; | ||
| 8 | import javafx.scene.control.Button; | ||
| 9 | import javafx.scene.control.CheckBox; | ||
| 10 | import javafx.scene.control.ComboBox; | ||
| 11 | import javafx.scene.control.Label; | ||
| 12 | import javafx.scene.control.TextArea; | ||
| 13 | import javafx.scene.control.TextField; | ||
| 14 | import market.guess.model.event.CommissionTiming; | ||
| 15 | import market.guess.model.event.CreateEventRequest; | ||
| 16 | import market.guess.model.event.MechanismType; | ||
| 17 | import market.guess.ui.desktop.util.Format; | ||
| 18 | import market.guess.ui.desktop.util.Views; | ||
| 19 | |||
| 20 | public class CreateEventDialogController { | ||
| 21 | private Runnable onCancel; | ||
| 22 | private Consumer<CreateEventRequest> onCreateRequest; | ||
| 23 | private BiConsumer<String, MechanismType> onCreate; | ||
| 24 | private MechanismType selectedType = MechanismType.LMSR; | ||
| 25 | |||
| 26 | @FXML private TextField nameField; | ||
| 27 | @FXML private TextArea descArea; | ||
| 28 | @FXML private Button lmsrBtn; | ||
| 29 | @FXML private Button orderBookBtn; | ||
| 30 | @FXML private ComboBox<String> feeCollectionCombo; | ||
| 31 | @FXML private TextField feePercentField; | ||
| 32 | @FXML private Label paramLabel; | ||
| 33 | @FXML private TextField paramField; | ||
| 34 | @FXML private TextField opt1Field; | ||
| 35 | @FXML private TextField opt2Field; | ||
| 36 | @FXML private CheckBox mintingCheckBox; | ||
| 37 | @FXML private Label infoNoteLabel; | ||
| 38 | |||
| 39 | @FXML | ||
| 40 | private void initialize() { | ||
| 41 | feeCollectionCombo.getItems().setAll("On purchase", "On close"); | ||
| 42 | feeCollectionCombo.setValue("On purchase"); | ||
| 43 | updateTypeSelection(); | ||
| 44 | } | ||
| 45 | |||
| 46 | public void setOnCancel(Runnable onCancel) { | ||
| 47 | this.onCancel = onCancel; | ||
| 48 | } | ||
| 49 | |||
| 50 | public void setOnCreate(Consumer<CreateEventRequest> onCreateRequest) { | ||
| 51 | this.onCreateRequest = onCreateRequest; | ||
| 52 | } | ||
| 53 | |||
| 54 | public void setOnCreate(BiConsumer<String, MechanismType> onCreate) { | ||
| 55 | this.onCreate = onCreate; | ||
| 56 | } | ||
| 57 | |||
| 58 | public void reset() { | ||
| 59 | nameField.setText("Will the summit happen before Q4?"); | ||
| 60 | descArea.setText( | ||
| 61 | "Resolves YES if a joint summit is publicly confirmed and held before October 1st."); | ||
| 62 | feeCollectionCombo.setValue("On purchase"); | ||
| 63 | feePercentField.setText("5"); | ||
| 64 | opt1Field.setText("YES"); | ||
| 65 | opt2Field.setText("NO"); | ||
| 66 | selectedType = MechanismType.LMSR; | ||
| 67 | updateTypeSelection(); | ||
| 68 | } | ||
| 69 | |||
| 70 | private void updateTypeSelection() { | ||
| 71 | Views.setSelected(lmsrBtn, selectedType == MechanismType.LMSR); | ||
| 72 | Views.setSelected(orderBookBtn, selectedType == MechanismType.ORDER_BOOK); | ||
| 73 | |||
| 74 | boolean isLmsr = selectedType == MechanismType.LMSR; | ||
| 75 | paramLabel.setText(isLmsr ? "Liquidity b (integer)" : "Base value d ($)"); | ||
| 76 | paramField.setText(isLmsr ? "100" : "1.00"); | ||
| 77 | mintingCheckBox.setVisible(!isLmsr); | ||
| 78 | |||
| 79 | String infoText = | ||
| 80 | isLmsr | ||
| 81 | ? "Opening this event will move " | ||
| 82 | + Format.money(BigDecimal.valueOf(100 * Math.log(2))) | ||
| 83 | + " of subsidy from your account into the contract account (b  ln 2)." | ||
| 84 | : "Opening this event will buy the initial share inventory from your account into the" | ||
| 85 | + " contract account."; | ||
| 86 | infoNoteLabel.setText(infoText); | ||
| 87 | } | ||
| 88 | |||
| 89 | @FXML | ||
| 90 | private void handleSelectLmsr() { | ||
| 91 | selectedType = MechanismType.LMSR; | ||
| 92 | updateTypeSelection(); | ||
| 93 | } | ||
| 94 | |||
| 95 | @FXML | ||
| 96 | private void handleSelectOrderBook() { | ||
| 97 | selectedType = MechanismType.ORDER_BOOK; | ||
| 98 | updateTypeSelection(); | ||
| 99 | } | ||
| 100 | |||
| 101 | @FXML | ||
| 102 | private void handleCancel() { | ||
| 103 | if (onCancel != null) { | ||
| 104 | onCancel.run(); | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | @FXML | ||
| 109 | private void handleCreate() { | ||
| 110 | String name = nameField.getText() != null ? nameField.getText().trim() : ""; | ||
| 111 | String desc = descArea.getText() != null ? descArea.getText().trim() : ""; | ||
| 112 | MechanismType type = selectedType; | ||
| 113 | CommissionTiming timing = "On close".equalsIgnoreCase(feeCollectionCombo.getValue()) | ||
| 114 | ? CommissionTiming.ON_CLOSE | ||
| 115 | : CommissionTiming.ON_PURCHASE; | ||
| 116 | |||
| 117 | int fee = 5; | ||
| 118 | try { | ||
| 119 | fee = Integer.parseInt(feePercentField.getText().trim()); | ||
| 120 | if (fee < 0) fee = 0; | ||
| 121 | if (fee > 90) fee = 90; | ||
| 122 | } catch (Exception ignored) { | ||
| 123 | } | ||
| 124 | |||
| 125 | Integer b = null; | ||
| 126 | BigDecimal d = null; | ||
| 127 | if (type == MechanismType.LMSR) { | ||
| 128 | try { | ||
| 129 | b = Integer.parseInt(paramField.getText().trim()); | ||
| 130 | } catch (Exception ignored) { | ||
| 131 | b = 100; | ||
| 132 | } | ||
| 133 | } else { | ||
| 134 | try { | ||
| 135 | d = new BigDecimal(paramField.getText().trim()); | ||
| 136 | } catch (Exception ignored) { | ||
| 137 | d = BigDecimal.ONE; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | String opt1 = opt1Field.getText() != null && !opt1Field.getText().isBlank() ? opt1Field.getText().trim() : "YES"; | ||
| 142 | String opt2 = opt2Field.getText() != null && !opt2Field.getText().isBlank() ? opt2Field.getText().trim() : "NO"; | ||
| 143 | boolean minting = mintingCheckBox.isSelected(); | ||
| 144 | |||
| 145 | var request = new CreateEventRequest( | ||
| 146 | name, | ||
| 147 | desc, | ||
| 148 | type, | ||
| 149 | fee, | ||
| 150 | timing, | ||
| 151 | null, | ||
| 152 | b, | ||
| 153 | d, | ||
| 154 | List.of(opt1, opt2), | ||
| 155 | minting); | ||
| 156 | |||
| 157 | if (onCreateRequest != null) { | ||
| 158 | onCreateRequest.accept(request); | ||
| 159 | } else if (onCreate != null) { | ||
| 160 | onCreate.accept(name, type); | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java new file mode 100644 index 0000000..9b01095 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EmptyStateController.java | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import javafx.fxml.FXML; | ||
| 4 | import market.guess.ui.desktop.components.graphic.MarketChart; | ||
| 5 | |||
| 6 | public class EmptyStateController { | ||
| 7 | @FXML private MarketChart marketChart; | ||
| 8 | |||
| 9 | public void setAnimating(boolean animating) { | ||
| 10 | if (marketChart != null) { | ||
| 11 | marketChart.setPlaying(animating); | ||
| 12 | } | ||
| 13 | } | ||
| 14 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java new file mode 100644 index 0000000..e71be40 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/EventsTabController.java | |||
| @@ -0,0 +1,496 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import java.util.ArrayList; | ||
| 4 | import java.util.function.Consumer; | ||
| 5 | import javafx.collections.ListChangeListener; | ||
| 6 | import javafx.css.PseudoClass; | ||
| 7 | import javafx.fxml.FXML; | ||
| 8 | import javafx.scene.Node; | ||
| 9 | import javafx.scene.chart.LineChart; | ||
| 10 | import javafx.scene.control.Button; | ||
| 11 | import javafx.scene.control.Label; | ||
| 12 | import javafx.scene.control.ListView; | ||
| 13 | import javafx.scene.control.ProgressBar; | ||
| 14 | import javafx.scene.layout.HBox; | ||
| 15 | import javafx.scene.layout.StackPane; | ||
| 16 | import javafx.scene.layout.VBox; | ||
| 17 | import market.guess.model.event.CommissionTiming; | ||
| 18 | import market.guess.model.event.EventStatus; | ||
| 19 | import market.guess.model.event.MechanismType; | ||
| 20 | import market.guess.ui.desktop.AppState; | ||
| 21 | import market.guess.ui.desktop.components.CommissionFilter; | ||
| 22 | import market.guess.ui.desktop.components.EventListCell; | ||
| 23 | import market.guess.ui.desktop.components.LadderItemView; | ||
| 24 | import market.guess.ui.desktop.components.MechanismFilter; | ||
| 25 | import market.guess.ui.desktop.components.ParticipantItemView; | ||
| 26 | import market.guess.ui.desktop.components.TradeHistoryItemView; | ||
| 27 | import market.guess.ui.desktop.components.graphic.EqualizerGraphic; | ||
| 28 | import market.guess.ui.desktop.components.graphic.Graphic; | ||
| 29 | import market.guess.ui.desktop.model.ChartPoint; | ||
| 30 | import market.guess.ui.desktop.model.EventData; | ||
| 31 | import market.guess.ui.desktop.model.OrderBook; | ||
| 32 | import market.guess.ui.desktop.model.ParticipantRow; | ||
| 33 | import market.guess.ui.desktop.model.TradeRow; | ||
| 34 | import market.guess.ui.desktop.util.Charts; | ||
| 35 | import market.guess.ui.desktop.util.Format; | ||
| 36 | import market.guess.ui.desktop.util.Views; | ||
| 37 | |||
| 38 | public class EventsTabController { | ||
| 39 | private static final PseudoClass ACTIVE = PseudoClass.getPseudoClass("active"); | ||
| 40 | private static final PseudoClass CLOSED = PseudoClass.getPseudoClass("closed"); | ||
| 41 | private static final PseudoClass IDLE = PseudoClass.getPseudoClass("idle"); | ||
| 42 | |||
| 43 | private AppState state; | ||
| 44 | private Runnable onNewEvent; | ||
| 45 | private Consumer<EventData> onOpenEvent; | ||
| 46 | private Runnable onCloseEvent; | ||
| 47 | private Graphic equalizer; | ||
| 48 | private boolean updatingSelection = false; | ||
| 49 | |||
| 50 | @FXML private Button methodFilterAllBtn; | ||
| 51 | @FXML private Button methodFilterLmsrBtn; | ||
| 52 | @FXML private Button methodFilterObBtn; | ||
| 53 | @FXML private Button statusFilterAllBtn; | ||
| 54 | @FXML private Button statusFilterNotStartedBtn; | ||
| 55 | @FXML private Button statusFilterActiveBtn; | ||
| 56 | @FXML private Button statusFilterClosedBtn; | ||
| 57 | @FXML private Button feeFilterAllBtn; | ||
| 58 | @FXML private Button feeFilterPurchaseBtn; | ||
| 59 | @FXML private Button feeFilterCloseBtn; | ||
| 60 | |||
| 61 | @FXML private ListView<EventData> eventListContainer; | ||
| 62 | @FXML private Label eventCountLabel; | ||
| 63 | |||
| 64 | @FXML private VBox eventDetailCard; | ||
| 65 | @FXML private Label eventNameLabel; | ||
| 66 | @FXML private Label eventDescLabel; | ||
| 67 | @FXML private Button openEventBtn; | ||
| 68 | @FXML private Button closeEventBtn; | ||
| 69 | |||
| 70 | @FXML private Label metaMethodLabel; | ||
| 71 | @FXML private Label metaStatusLabel; | ||
| 72 | @FXML private HBox livePill; | ||
| 73 | @FXML private StackPane liveBarsBox; | ||
| 74 | @FXML private Label liveLabel; | ||
| 75 | @FXML private Label metaMmLabel; | ||
| 76 | @FXML private Label metaFeeLabel; | ||
| 77 | @FXML private Label metaContractLabel; | ||
| 78 | |||
| 79 | @FXML private VBox lmsrSection; | ||
| 80 | @FXML private Label lmsrYesPriceLabel; | ||
| 81 | @FXML private ProgressBar lmsrYesProgressBar; | ||
| 82 | @FXML private Label lmsrYesSharesLabel; | ||
| 83 | @FXML private Label lmsrNoPriceLabel; | ||
| 84 | @FXML private ProgressBar lmsrNoProgressBar; | ||
| 85 | @FXML private Label lmsrNoSharesLabel; | ||
| 86 | @FXML private Label lmsrChartSubLabel; | ||
| 87 | @FXML private LineChart<Number, Number> lmsrChart; | ||
| 88 | @FXML private VBox tradeHistoryRowsContainer; | ||
| 89 | |||
| 90 | @FXML private VBox orderBookSection; | ||
| 91 | @FXML private Label yesBookCountLabel; | ||
| 92 | @FXML private Label yesLastLabel; | ||
| 93 | @FXML private Label yesMidLabel; | ||
| 94 | @FXML private Label yesBestBidLabel; | ||
| 95 | @FXML private Label yesBestAskLabel; | ||
| 96 | @FXML private Label yesSpreadLabel; | ||
| 97 | @FXML private VBox yesBookLadderList; | ||
| 98 | |||
| 99 | @FXML private Label noBookCountLabel; | ||
| 100 | @FXML private Label noLastLabel; | ||
| 101 | @FXML private Label noMidLabel; | ||
| 102 | @FXML private Label noBestBidLabel; | ||
| 103 | @FXML private Label noBestAskLabel; | ||
| 104 | @FXML private Label noSpreadLabel; | ||
| 105 | @FXML private VBox noBookLadderList; | ||
| 106 | |||
| 107 | @FXML private Label obChartSubLabel; | ||
| 108 | @FXML private LineChart<Number, Number> obChart; | ||
| 109 | |||
| 110 | @FXML private VBox participantsRowsContainer; | ||
| 111 | @FXML private HBox resolvedBanner; | ||
| 112 | @FXML private Label resolvedBannerText; | ||
| 113 | |||
| 114 | public void init(AppState state) { | ||
| 115 | this.state = state; | ||
| 116 | equalizer = EqualizerGraphic.create(); | ||
| 117 | liveBarsBox.getChildren().setAll(equalizer.getNode()); | ||
| 118 | |||
| 119 | eventListContainer.setCellFactory(lv -> new EventListCell()); | ||
| 120 | eventListContainer | ||
| 121 | .getSelectionModel() | ||
| 122 | .selectedItemProperty() | ||
| 123 | .addListener( | ||
| 124 | (obs, oldVal, newVal) -> { | ||
| 125 | if (updatingSelection) return; | ||
| 126 | if (newVal != null && state != null) { | ||
| 127 | state.setSelectedEvent(newVal); | ||
| 128 | } | ||
| 129 | }); | ||
| 130 | |||
| 131 | state | ||
| 132 | .selectedEventProperty() | ||
| 133 | .addListener( | ||
| 134 | (obs, oldVal, newVal) -> { | ||
| 135 | if (newVal != null) { | ||
| 136 | if (!newVal.equals(eventListContainer.getSelectionModel().getSelectedItem())) { | ||
| 137 | updatingSelection = true; | ||
| 138 | try { | ||
| 139 | eventListContainer.getSelectionModel().select(newVal); | ||
| 140 | } finally { | ||
| 141 | updatingSelection = false; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | refreshDetailCard(newVal); | ||
| 146 | }); | ||
| 147 | |||
| 148 | state.filterMethodProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); | ||
| 149 | state.filterStatusProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); | ||
| 150 | state.filterFeeProperty().addListener((obs, oldVal, newVal) -> refreshListAndFilters()); | ||
| 151 | state | ||
| 152 | .getEvents() | ||
| 153 | .addListener( | ||
| 154 | (ListChangeListener<EventData>) | ||
| 155 | c -> { | ||
| 156 | refreshListAndFilters(); | ||
| 157 | refreshDetailCard(state.selectedEvent()); | ||
| 158 | }); | ||
| 159 | state | ||
| 160 | .animationsOnProperty() | ||
| 161 | .addListener( | ||
| 162 | (obs, oldVal, newVal) -> { | ||
| 163 | var sel = state.selectedEvent(); | ||
| 164 | boolean isActive = sel != null && sel.status == EventStatus.ACTIVE; | ||
| 165 | equalizer.setPlaying(newVal && isActive); | ||
| 166 | }); | ||
| 167 | } | ||
| 168 | |||
| 169 | public void setOnNewEvent(Runnable onNewEvent) { | ||
| 170 | this.onNewEvent = onNewEvent; | ||
| 171 | } | ||
| 172 | |||
| 173 | public void setOnOpenEvent(Consumer<EventData> onOpenEvent) { | ||
| 174 | this.onOpenEvent = onOpenEvent; | ||
| 175 | } | ||
| 176 | |||
| 177 | public void setOnCloseEvent(Runnable onCloseEvent) { | ||
| 178 | this.onCloseEvent = onCloseEvent; | ||
| 179 | } | ||
| 180 | |||
| 181 | public void refresh() { | ||
| 182 | if (state == null) return; | ||
| 183 | refreshListAndFilters(); | ||
| 184 | refreshDetailCard(state.selectedEvent()); | ||
| 185 | } | ||
| 186 | |||
| 187 | private void refreshListAndFilters() { | ||
| 188 | if (state == null) return; | ||
| 189 | |||
| 190 | // 1. Update filter button selections | ||
| 191 | Views.setSelected(methodFilterAllBtn, state.getFilterMethod() == MechanismFilter.ALL); | ||
| 192 | Views.setSelected(methodFilterLmsrBtn, state.getFilterMethod() == MechanismFilter.LMSR); | ||
| 193 | Views.setSelected(methodFilterObBtn, state.getFilterMethod() == MechanismFilter.ORDER_BOOK); | ||
| 194 | |||
| 195 | Views.setSelected(statusFilterAllBtn, state.getFilterStatus() == null); | ||
| 196 | Views.setSelected(statusFilterNotStartedBtn, state.getFilterStatus() == EventStatus.DRAFT); | ||
| 197 | Views.setSelected(statusFilterActiveBtn, state.getFilterStatus() == EventStatus.ACTIVE); | ||
| 198 | Views.setSelected(statusFilterClosedBtn, state.getFilterStatus() == EventStatus.SETTLED); | ||
| 199 | |||
| 200 | Views.setSelected(feeFilterAllBtn, state.getFilterFee() == CommissionFilter.ALL); | ||
| 201 | Views.setSelected(feeFilterPurchaseBtn, state.getFilterFee() == CommissionFilter.PURCHASE); | ||
| 202 | Views.setSelected(feeFilterCloseBtn, state.getFilterFee() == CommissionFilter.CLOSE); | ||
| 203 | |||
| 204 | // 2. Update list | ||
| 205 | var filtered = | ||
| 206 | state.getEvents().stream() | ||
| 207 | .filter( | ||
| 208 | e -> | ||
| 209 | state.getFilterMethod() == MechanismFilter.ALL | ||
| 210 | || (state.getFilterMethod() == MechanismFilter.LMSR | ||
| 211 | && e.type == MechanismType.LMSR) | ||
| 212 | || (state.getFilterMethod() == MechanismFilter.ORDER_BOOK | ||
| 213 | && e.type == MechanismType.ORDER_BOOK)) | ||
| 214 | .filter(e -> state.getFilterStatus() == null || e.status == state.getFilterStatus()) | ||
| 215 | .filter( | ||
| 216 | e -> | ||
| 217 | state.getFilterFee() == CommissionFilter.ALL | ||
| 218 | || (state.getFilterFee() == CommissionFilter.PURCHASE | ||
| 219 | && e.feeMode == CommissionTiming.ON_PURCHASE) | ||
| 220 | || (state.getFilterFee() == CommissionFilter.CLOSE | ||
| 221 | && e.feeMode == CommissionTiming.ON_CLOSE)) | ||
| 222 | .toList(); | ||
| 223 | |||
| 224 | updatingSelection = true; | ||
| 225 | try { | ||
| 226 | eventListContainer.getItems().setAll(filtered); | ||
| 227 | eventCountLabel.setText( | ||
| 228 | filtered.size() + " of " + state.getEvents().size() + " events shown"); | ||
| 229 | |||
| 230 | var sel = state.selectedEvent(); | ||
| 231 | EventData match = null; | ||
| 232 | if (sel != null) { | ||
| 233 | for (var e : filtered) { | ||
| 234 | if (e.id.equals(sel.id)) { | ||
| 235 | match = e; | ||
| 236 | break; | ||
| 237 | } | ||
| 238 | } | ||
| 239 | } | ||
| 240 | if (match != null) { | ||
| 241 | eventListContainer.getSelectionModel().select(match); | ||
| 242 | } else if (!filtered.isEmpty()) { | ||
| 243 | eventListContainer.getSelectionModel().select(0); | ||
| 244 | } | ||
| 245 | } finally { | ||
| 246 | updatingSelection = false; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | private void refreshDetailCard(EventData selected) { | ||
| 251 | if (selected == null) { | ||
| 252 | eventDetailCard.setVisible(false); | ||
| 253 | return; | ||
| 254 | } | ||
| 255 | eventDetailCard.setVisible(true); | ||
| 256 | |||
| 257 | eventNameLabel.setText(selected.num + ". " + selected.name); | ||
| 258 | eventDescLabel.setText(selected.desc); | ||
| 259 | |||
| 260 | // Open = go trade: any unblocked user on an ACTIVE event, or the MM activating their DRAFT. | ||
| 261 | var actor = state.getActingUser(); | ||
| 262 | boolean isMm = selected.mm.equals(state.getActingUserName()); | ||
| 263 | boolean canOpen = | ||
| 264 | actor != null | ||
| 265 | && !actor.blocked | ||
| 266 | && (selected.status == EventStatus.ACTIVE | ||
| 267 | || (selected.status == EventStatus.DRAFT && isMm)); | ||
| 268 | boolean canClose = | ||
| 269 | selected.status == EventStatus.ACTIVE && selected.mm.equals(state.getActingUserName()); | ||
| 270 | |||
| 271 | openEventBtn.setText(selected.status == EventStatus.DRAFT ? "Start Event" : "Trade"); | ||
| 272 | openEventBtn.setVisible(canOpen); | ||
| 273 | openEventBtn.setManaged(canOpen); | ||
| 274 | closeEventBtn.setVisible(canClose); | ||
| 275 | closeEventBtn.setManaged(canClose); | ||
| 276 | |||
| 277 | // Meta fields | ||
| 278 | metaMethodLabel.setText(selected.type == MechanismType.LMSR ? "LMSR" : "Order Book"); | ||
| 279 | metaStatusLabel.setText(selected.status.name()); | ||
| 280 | |||
| 281 | // LIVE pill: only an ACTIVE market needs to look like order flow is arriving. | ||
| 282 | boolean isActive = selected.status == EventStatus.ACTIVE; | ||
| 283 | boolean isEnded = selected.status == EventStatus.SETTLED; | ||
| 284 | liveLabel.setText(isActive ? "LIVE" : isEnded ? "ENDED" : "IDLE"); | ||
| 285 | livePill.pseudoClassStateChanged(ACTIVE, isActive); | ||
| 286 | livePill.pseudoClassStateChanged(CLOSED, isEnded); | ||
| 287 | livePill.pseudoClassStateChanged(IDLE, !isActive && !isEnded); | ||
| 288 | equalizer.setPlaying(state.isAnimationsOn() && isActive); | ||
| 289 | metaMmLabel.setText(selected.mm); | ||
| 290 | metaFeeLabel.setText(selected.feeText()); | ||
| 291 | metaContractLabel.setText(Format.money(selected.contract)); | ||
| 292 | |||
| 293 | // Sections | ||
| 294 | boolean isLmsr = selected.type == MechanismType.LMSR; | ||
| 295 | lmsrSection.setVisible(isLmsr); | ||
| 296 | lmsrSection.setManaged(isLmsr); | ||
| 297 | orderBookSection.setVisible(!isLmsr); | ||
| 298 | orderBookSection.setManaged(!isLmsr); | ||
| 299 | |||
| 300 | if (isLmsr) { | ||
| 301 | refreshLmsr(selected); | ||
| 302 | } else { | ||
| 303 | refreshOrderBook(selected); | ||
| 304 | } | ||
| 305 | |||
| 306 | // Trade history table | ||
| 307 | var tRows = new ArrayList<Node>(selected.trades.size()); | ||
| 308 | for (var t : selected.trades) { | ||
| 309 | tRows.add(buildTradeHistoryRow(t)); | ||
| 310 | } | ||
| 311 | tradeHistoryRowsContainer.getChildren().setAll(tRows); | ||
| 312 | |||
| 313 | // Participants table | ||
| 314 | var pRows = new ArrayList<Node>(selected.participants.size()); | ||
| 315 | for (var p : selected.participants) { | ||
| 316 | pRows.add(buildParticipantRow(p)); | ||
| 317 | } | ||
| 318 | participantsRowsContainer.getChildren().setAll(pRows); | ||
| 319 | |||
| 320 | // Resolved banner | ||
| 321 | boolean isClosed = selected.status == EventStatus.SETTLED; | ||
| 322 | resolvedBanner.setVisible(isClosed); | ||
| 323 | resolvedBanner.setManaged(isClosed); | ||
| 324 | if (isClosed) { | ||
| 325 | resolvedBannerText.setText( | ||
| 326 | "Winning option: " | ||
| 327 | + selected.resolvedOption | ||
| 328 | + "  " | ||
| 329 | + selected.yesShares | ||
| 330 | + " YES / " | ||
| 331 | + selected.noShares | ||
| 332 | + " NO shares bought  contract emptied to holders, " | ||
| 333 | + selected.feePercent | ||
| 334 | + "% fee moved to " | ||
| 335 | + selected.mm | ||
| 336 | + "."); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | private void refreshLmsr(EventData e) { | ||
| 341 | lmsrYesPriceLabel.setText(Format.money(e.yesPrice)); | ||
| 342 | lmsrYesProgressBar.setProgress(e.yesPrice.doubleValue()); | ||
| 343 | lmsrYesSharesLabel.setText( | ||
| 344 | e.yesShares + " shares  p = " + Format.prob(e.yesPrice.doubleValue())); | ||
| 345 | |||
| 346 | lmsrNoPriceLabel.setText(Format.money(e.noPrice)); | ||
| 347 | lmsrNoProgressBar.setProgress(e.noPrice.doubleValue()); | ||
| 348 | lmsrNoSharesLabel.setText(e.noShares + " shares  p = " + Format.prob(e.noPrice.doubleValue())); | ||
| 349 | |||
| 350 | lmsrChartSubLabel.setText("YES / NO  LMSR b = " + e.liquidityB); | ||
| 351 | var noPts = new ArrayList<ChartPoint>(); | ||
| 352 | for (var p : e.chart) noPts.add(new ChartPoint(p.x(), 1.0 - p.y())); | ||
| 353 | Charts.setDualSeries(lmsrChart, "YES", e.chart, "NO", noPts); | ||
| 354 | } | ||
| 355 | |||
| 356 | private void refreshOrderBook(EventData e) { | ||
| 357 | obChartSubLabel.setText( | ||
| 358 | "last trade YES  base d = " + (e.baseValueD != null ? Format.money(e.baseValueD) : "—")); | ||
| 359 | Charts.setSingleSeries(obChart, "value", e.chart); | ||
| 360 | |||
| 361 | populateBookSide( | ||
| 362 | e.yesBook, | ||
| 363 | yesBookCountLabel, | ||
| 364 | yesLastLabel, | ||
| 365 | yesBestBidLabel, | ||
| 366 | yesBestAskLabel, | ||
| 367 | yesMidLabel, | ||
| 368 | yesSpreadLabel, | ||
| 369 | yesBookLadderList); | ||
| 370 | populateBookSide( | ||
| 371 | e.noBook, | ||
| 372 | noBookCountLabel, | ||
| 373 | noLastLabel, | ||
| 374 | noBestBidLabel, | ||
| 375 | noBestAskLabel, | ||
| 376 | noMidLabel, | ||
| 377 | noSpreadLabel, | ||
| 378 | noBookLadderList); | ||
| 379 | } | ||
| 380 | |||
| 381 | private void populateBookSide( | ||
| 382 | OrderBook book, | ||
| 383 | Label countLabel, | ||
| 384 | Label lastLabel, | ||
| 385 | Label bestBidLabel, | ||
| 386 | Label bestAskLabel, | ||
| 387 | Label midLabel, | ||
| 388 | Label spreadLabel, | ||
| 389 | VBox ladderList) { | ||
| 390 | if (book == null) { | ||
| 391 | ladderList.getChildren().clear(); | ||
| 392 | countLabel.setText("0 orders"); | ||
| 393 | for (var l : new Label[] {lastLabel, bestBidLabel, bestAskLabel, midLabel, spreadLabel}) { | ||
| 394 | l.setText("—"); | ||
| 395 | } | ||
| 396 | return; | ||
| 397 | } | ||
| 398 | |||
| 399 | countLabel.setText(book.rows.size() + " orders"); | ||
| 400 | lastLabel.setText(book.last != null ? book.last : "—"); | ||
| 401 | bestBidLabel.setText(book.bid != null ? book.bid : "—"); | ||
| 402 | bestAskLabel.setText(book.ask != null ? book.ask : "—"); | ||
| 403 | midLabel.setText(book.mid != null ? book.mid : "—"); | ||
| 404 | spreadLabel.setText(book.spread != null ? book.spread : "—"); | ||
| 405 | |||
| 406 | var rows = new ArrayList<Node>(book.rows.size()); | ||
| 407 | for (var r : book.rows) { | ||
| 408 | rows.add(new LadderItemView(r)); | ||
| 409 | } | ||
| 410 | ladderList.getChildren().setAll(rows); | ||
| 411 | } | ||
| 412 | |||
| 413 | private Node buildTradeHistoryRow(TradeRow t) { | ||
| 414 | return new TradeHistoryItemView(t); | ||
| 415 | } | ||
| 416 | |||
| 417 | private Node buildParticipantRow(ParticipantRow p) { | ||
| 418 | return new ParticipantItemView(p); | ||
| 419 | } | ||
| 420 | |||
| 421 | // ---- FXML Handlers ---------------------------------------------------- | ||
| 422 | |||
| 423 | @FXML | ||
| 424 | private void handleMethodAll() { | ||
| 425 | if (state != null) state.setFilterMethod(MechanismFilter.ALL); | ||
| 426 | } | ||
| 427 | |||
| 428 | @FXML | ||
| 429 | private void handleMethodLmsr() { | ||
| 430 | if (state != null) state.setFilterMethod(MechanismFilter.LMSR); | ||
| 431 | } | ||
| 432 | |||
| 433 | @FXML | ||
| 434 | private void handleMethodOb() { | ||
| 435 | if (state != null) state.setFilterMethod(MechanismFilter.ORDER_BOOK); | ||
| 436 | } | ||
| 437 | |||
| 438 | @FXML | ||
| 439 | private void handleStatusAll() { | ||
| 440 | if (state != null) state.setFilterStatus(null); | ||
| 441 | } | ||
| 442 | |||
| 443 | @FXML | ||
| 444 | private void handleStatusNotStarted() { | ||
| 445 | if (state != null) state.setFilterStatus(EventStatus.DRAFT); | ||
| 446 | } | ||
| 447 | |||
| 448 | @FXML | ||
| 449 | private void handleStatusActive() { | ||
| 450 | if (state != null) state.setFilterStatus(EventStatus.ACTIVE); | ||
| 451 | } | ||
| 452 | |||
| 453 | @FXML | ||
| 454 | private void handleStatusClosed() { | ||
| 455 | if (state != null) state.setFilterStatus(EventStatus.SETTLED); | ||
| 456 | } | ||
| 457 | |||
| 458 | @FXML | ||
| 459 | private void handleFeeAll() { | ||
| 460 | if (state != null) state.setFilterFee(CommissionFilter.ALL); | ||
| 461 | } | ||
| 462 | |||
| 463 | @FXML | ||
| 464 | private void handleFeePurchase() { | ||
| 465 | if (state != null) state.setFilterFee(CommissionFilter.PURCHASE); | ||
| 466 | } | ||
| 467 | |||
| 468 | @FXML | ||
| 469 | private void handleFeeClose() { | ||
| 470 | if (state != null) state.setFilterFee(CommissionFilter.CLOSE); | ||
| 471 | } | ||
| 472 | |||
| 473 | @FXML | ||
| 474 | private void handleNewEvent() { | ||
| 475 | if (onNewEvent != null) { | ||
| 476 | onNewEvent.run(); | ||
| 477 | } | ||
| 478 | } | ||
| 479 | |||
| 480 | @FXML | ||
| 481 | private void handleOpenEvent() { | ||
| 482 | if (state != null) { | ||
| 483 | var e = state.selectedEvent(); | ||
| 484 | if (e != null && onOpenEvent != null) { | ||
| 485 | onOpenEvent.accept(e); | ||
| 486 | } | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | @FXML | ||
| 491 | private void handleCloseEvent() { | ||
| 492 | if (onCloseEvent != null) { | ||
| 493 | onCloseEvent.run(); | ||
| 494 | } | ||
| 495 | } | ||
| 496 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java new file mode 100644 index 0000000..e23cd9e --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/LoadDialogController.java | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import javafx.animation.KeyFrame; | ||
| 4 | import javafx.animation.KeyValue; | ||
| 5 | import javafx.animation.Timeline; | ||
| 6 | import javafx.fxml.FXML; | ||
| 7 | import javafx.scene.control.Label; | ||
| 8 | import javafx.scene.control.ProgressBar; | ||
| 9 | import javafx.util.Duration; | ||
| 10 | import market.guess.ui.desktop.task.InitialLoadTask; | ||
| 11 | |||
| 12 | public final class LoadDialogController { | ||
| 13 | private Runnable onCancel; | ||
| 14 | private Timeline currentFillAnimation; | ||
| 15 | |||
| 16 | @FXML private Label pathLabel; | ||
| 17 | @FXML private ProgressBar progressBar; | ||
| 18 | |||
| 19 | public void setOnCancel(Runnable onCancel) { | ||
| 20 | this.onCancel = onCancel; | ||
| 21 | } | ||
| 22 | |||
| 23 | public void reset() { | ||
| 24 | if (currentFillAnimation != null) { | ||
| 25 | currentFillAnimation.stop(); | ||
| 26 | currentFillAnimation = null; | ||
| 27 | } | ||
| 28 | if (progressBar != null) { | ||
| 29 | progressBar.setProgress(0.0); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | public void show(InitialLoadTask task, String pendingFile) { | ||
| 34 | reset(); | ||
| 35 | if (pathLabel != null) { | ||
| 36 | pathLabel.setText(pendingFile); | ||
| 37 | } | ||
| 38 | task.progressProperty() | ||
| 39 | .addListener( | ||
| 40 | (observable, oldValue, newValue) -> { | ||
| 41 | if (newValue == null) return; | ||
| 42 | double val = newValue.doubleValue(); | ||
| 43 | if (val < 0) { | ||
| 44 | if (progressBar != null) { | ||
| 45 | progressBar.setProgress(0.0); | ||
| 46 | } | ||
| 47 | return; | ||
| 48 | } | ||
| 49 | if (progressBar != null) { | ||
| 50 | if (currentFillAnimation != null) { | ||
| 51 | currentFillAnimation.stop(); | ||
| 52 | } | ||
| 53 | currentFillAnimation = | ||
| 54 | new Timeline( | ||
| 55 | new KeyFrame( | ||
| 56 | Duration.millis(100), | ||
| 57 | new KeyValue(progressBar.progressProperty(), val))); | ||
| 58 | currentFillAnimation.play(); | ||
| 59 | } | ||
| 60 | }); | ||
| 61 | } | ||
| 62 | |||
| 63 | @FXML | ||
| 64 | private void handleCancel() { | ||
| 65 | reset(); | ||
| 66 | if (onCancel != null) { | ||
| 67 | onCancel.run(); | ||
| 68 | } | ||
| 69 | } | ||
| 70 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java new file mode 100644 index 0000000..4183f7f --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/ResolveDialogController.java | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import java.util.function.Consumer; | ||
| 4 | import javafx.fxml.FXML; | ||
| 5 | import javafx.scene.control.Button; | ||
| 6 | import market.guess.ui.desktop.util.Views; | ||
| 7 | |||
| 8 | public class ResolveDialogController { | ||
| 9 | private Runnable onCancel; | ||
| 10 | private Consumer<String> onConfirm; | ||
| 11 | private String selectedOption = null; | ||
| 12 | |||
| 13 | @FXML private Button resolveYesBtn; | ||
| 14 | @FXML private Button resolveNoBtn; | ||
| 15 | @FXML private Button confirmBtn; | ||
| 16 | |||
| 17 | @FXML | ||
| 18 | private void initialize() { | ||
| 19 | updateSelection(); | ||
| 20 | } | ||
| 21 | |||
| 22 | public void setOnCancel(Runnable onCancel) { | ||
| 23 | this.onCancel = onCancel; | ||
| 24 | } | ||
| 25 | |||
| 26 | public void setOnConfirm(Consumer<String> onConfirm) { | ||
| 27 | this.onConfirm = onConfirm; | ||
| 28 | } | ||
| 29 | |||
| 30 | public void reset() { | ||
| 31 | selectedOption = null; | ||
| 32 | updateSelection(); | ||
| 33 | } | ||
| 34 | |||
| 35 | private void updateSelection() { | ||
| 36 | Views.setSelected(resolveYesBtn, "YES".equals(selectedOption)); | ||
| 37 | Views.setSelected(resolveNoBtn, "NO".equals(selectedOption)); | ||
| 38 | boolean hasChoice = selectedOption != null; | ||
| 39 | confirmBtn.setDisable(!hasChoice); | ||
| 40 | confirmBtn.setText("Resolve as " + (hasChoice ? selectedOption : "…")); | ||
| 41 | } | ||
| 42 | |||
| 43 | @FXML | ||
| 44 | private void handleSelectYes() { | ||
| 45 | selectedOption = "YES"; | ||
| 46 | updateSelection(); | ||
| 47 | } | ||
| 48 | |||
| 49 | @FXML | ||
| 50 | private void handleSelectNo() { | ||
| 51 | selectedOption = "NO"; | ||
| 52 | updateSelection(); | ||
| 53 | } | ||
| 54 | |||
| 55 | @FXML | ||
| 56 | private void handleCancel() { | ||
| 57 | if (onCancel != null) { | ||
| 58 | onCancel.run(); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | @FXML | ||
| 63 | private void handleConfirm() { | ||
| 64 | if (onConfirm != null && selectedOption != null) { | ||
| 65 | onConfirm.accept(selectedOption); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java new file mode 100644 index 0000000..6211337 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/controllers/UsersTabController.java | |||
| @@ -0,0 +1,476 @@ | |||
| 1 | package market.guess.ui.desktop.controllers; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.util.ArrayList; | ||
| 5 | import java.util.function.Consumer; | ||
| 6 | import javafx.collections.ListChangeListener; | ||
| 7 | import javafx.css.PseudoClass; | ||
| 8 | import javafx.fxml.FXML; | ||
| 9 | import javafx.scene.Node; | ||
| 10 | import javafx.scene.chart.LineChart; | ||
| 11 | import javafx.scene.chart.NumberAxis; | ||
| 12 | import javafx.scene.control.Button; | ||
| 13 | import javafx.scene.control.Label; | ||
| 14 | import javafx.scene.control.ListView; | ||
| 15 | import javafx.scene.control.TextField; | ||
| 16 | import javafx.scene.layout.HBox; | ||
| 17 | import javafx.scene.layout.StackPane; | ||
| 18 | import javafx.scene.layout.VBox; | ||
| 19 | import market.guess.model.event.EventStatus; | ||
| 20 | import market.guess.model.event.MechanismType; | ||
| 21 | import market.guess.ui.desktop.AppState; | ||
| 22 | import market.guess.ui.desktop.components.TradeSide; | ||
| 23 | import market.guess.ui.desktop.components.UserEventItemView; | ||
| 24 | import market.guess.ui.desktop.components.UserListCell; | ||
| 25 | import market.guess.ui.desktop.components.graphic.Graphic; | ||
| 26 | import market.guess.ui.desktop.components.graphic.SparklineGraphic; | ||
| 27 | import market.guess.ui.desktop.model.UserData; | ||
| 28 | import market.guess.ui.desktop.model.UserEventRow; | ||
| 29 | import market.guess.ui.desktop.util.Charts; | ||
| 30 | import market.guess.ui.desktop.util.Format; | ||
| 31 | import market.guess.ui.desktop.util.Views; | ||
| 32 | |||
| 33 | public class UsersTabController { | ||
| 34 | private static final PseudoClass BLOCKED = PseudoClass.getPseudoClass("blocked"); | ||
| 35 | private static final PseudoClass MM = PseudoClass.getPseudoClass("mm"); | ||
| 36 | private static final PseudoClass TRADER = PseudoClass.getPseudoClass("trader"); | ||
| 37 | private static final PseudoClass NEGATIVE = PseudoClass.getPseudoClass("negative"); | ||
| 38 | private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); | ||
| 39 | |||
| 40 | @FunctionalInterface | ||
| 41 | public interface OrderPlacer { | ||
| 42 | void placeOrder( | ||
| 43 | String eventKey, String optionKey, String side, BigDecimal price, long quantity); | ||
| 44 | } | ||
| 45 | |||
| 46 | private AppState state; | ||
| 47 | private Runnable onCreateNewEvent; | ||
| 48 | private Consumer<String> onToast; | ||
| 49 | private OrderPlacer onPlaceOrder; | ||
| 50 | private Graphic sparkline; | ||
| 51 | private boolean updatingSelection = false; | ||
| 52 | |||
| 53 | @FXML private StackPane sparklineBox; | ||
| 54 | @FXML private ListView<UserData> userListContainer; | ||
| 55 | @FXML private VBox userDetailCard; | ||
| 56 | @FXML private Label userNameLabel; | ||
| 57 | @FXML private Label userRolePill; | ||
| 58 | @FXML private Label userSubLabel; | ||
| 59 | @FXML private Label userBalanceLabel; | ||
| 60 | |||
| 61 | @FXML private HBox blockedBanner; | ||
| 62 | @FXML private LineChart<Number, Number> balanceChart; | ||
| 63 | @FXML private VBox participationRowsContainer; | ||
| 64 | |||
| 65 | @FXML private VBox tradePanel; | ||
| 66 | @FXML private Label noTradeEventLabel; | ||
| 67 | @FXML private VBox tradeContentBox; | ||
| 68 | @FXML private Label tradeEventTitle; | ||
| 69 | @FXML private Label tradeEventSub; | ||
| 70 | @FXML private VBox tradeYesOptCard; | ||
| 71 | @FXML private Label tradeYesPriceLabel; | ||
| 72 | @FXML private Label tradeYesHeldLabel; | ||
| 73 | @FXML private VBox tradeNoOptCard; | ||
| 74 | @FXML private Label tradeNoPriceLabel; | ||
| 75 | @FXML private Label tradeNoHeldLabel; | ||
| 76 | @FXML private Button tradeBuyBtn; | ||
| 77 | @FXML private Button tradeSellBtn; | ||
| 78 | @FXML private TextField tradeQtyField; | ||
| 79 | @FXML private TextField tradePriceField; | ||
| 80 | @FXML private Label tradeCostLabel; | ||
| 81 | @FXML private Button tradeSubmitBtn; | ||
| 82 | @FXML private Label tradeHintLabel; | ||
| 83 | |||
| 84 | public void init(AppState state) { | ||
| 85 | this.state = state; | ||
| 86 | sparkline = SparklineGraphic.create(); | ||
| 87 | sparklineBox.getChildren().setAll(sparkline.node); | ||
| 88 | |||
| 89 | userListContainer.setCellFactory(lv -> new UserListCell()); | ||
| 90 | userListContainer | ||
| 91 | .getSelectionModel() | ||
| 92 | .selectedItemProperty() | ||
| 93 | .addListener( | ||
| 94 | (obs, oldVal, newVal) -> { | ||
| 95 | if (updatingSelection) return; | ||
| 96 | if (newVal != null && state != null) { | ||
| 97 | state.setActingUser(newVal); | ||
| 98 | } | ||
| 99 | }); | ||
| 100 | |||
| 101 | state | ||
| 102 | .actingUserProperty() | ||
| 103 | .addListener( | ||
| 104 | (obs, oldVal, newVal) -> { | ||
| 105 | if (newVal != null) { | ||
| 106 | if (!newVal.equals(userListContainer.getSelectionModel().getSelectedItem())) { | ||
| 107 | updatingSelection = true; | ||
| 108 | try { | ||
| 109 | userListContainer.getSelectionModel().select(newVal); | ||
| 110 | } finally { | ||
| 111 | updatingSelection = false; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | refreshUserDetail(newVal); | ||
| 116 | }); | ||
| 117 | |||
| 118 | state | ||
| 119 | .selectedEventProperty() | ||
| 120 | .addListener( | ||
| 121 | (obs, oldVal, newVal) -> { | ||
| 122 | refreshTradePanel(state.actingUser()); | ||
| 123 | }); | ||
| 124 | |||
| 125 | state | ||
| 126 | .tradeOptionYesProperty() | ||
| 127 | .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser())); | ||
| 128 | |||
| 129 | state | ||
| 130 | .tradeSideProperty() | ||
| 131 | .addListener((obs, oldVal, newVal) -> refreshTradePanel(state.actingUser())); | ||
| 132 | |||
| 133 | state | ||
| 134 | .getUsers() | ||
| 135 | .addListener( | ||
| 136 | (ListChangeListener<UserData>) | ||
| 137 | c -> { | ||
| 138 | refreshUserList(); | ||
| 139 | refreshUserDetail(state.actingUser()); | ||
| 140 | }); | ||
| 141 | state.animationsOnProperty().addListener((obs, oldVal, newVal) -> sparkline.setPlaying(newVal)); | ||
| 142 | |||
| 143 | tradeQtyField | ||
| 144 | .textProperty() | ||
| 145 | .addListener( | ||
| 146 | (obs, o, n) -> { | ||
| 147 | if (state != null) state.setTradeQty(n); | ||
| 148 | validateTradeInputs(); | ||
| 149 | }); | ||
| 150 | |||
| 151 | tradePriceField | ||
| 152 | .textProperty() | ||
| 153 | .addListener( | ||
| 154 | (obs, o, n) -> { | ||
| 155 | if (state != null) state.setTradePrice(n); | ||
| 156 | validateTradeInputs(); | ||
| 157 | }); | ||
| 158 | } | ||
| 159 | |||
| 160 | public void setOnCreateNewEvent(Runnable onCreateNewEvent) { | ||
| 161 | this.onCreateNewEvent = onCreateNewEvent; | ||
| 162 | } | ||
| 163 | |||
| 164 | public void setOnToast(Consumer<String> onToast) { | ||
| 165 | this.onToast = onToast; | ||
| 166 | } | ||
| 167 | |||
| 168 | public void setOnPlaceOrder(OrderPlacer onPlaceOrder) { | ||
| 169 | this.onPlaceOrder = onPlaceOrder; | ||
| 170 | } | ||
| 171 | |||
| 172 | public void refresh() { | ||
| 173 | if (state == null) return; | ||
| 174 | refreshUserList(); | ||
| 175 | refreshUserDetail(state.actingUser()); | ||
| 176 | } | ||
| 177 | |||
| 178 | private void refreshUserList() { | ||
| 179 | if (state == null) return; | ||
| 180 | updatingSelection = true; | ||
| 181 | try { | ||
| 182 | userListContainer.getItems().setAll(state.getUsers()); | ||
| 183 | var actor = state.actingUser(); | ||
| 184 | UserData match = null; | ||
| 185 | if (actor != null) { | ||
| 186 | for (var u : state.getUsers()) { | ||
| 187 | if (u.name.equals(actor.name)) { | ||
| 188 | match = u; | ||
| 189 | break; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | if (match != null) { | ||
| 194 | userListContainer.getSelectionModel().select(match); | ||
| 195 | } else if (!state.getUsers().isEmpty()) { | ||
| 196 | userListContainer.getSelectionModel().select(0); | ||
| 197 | } | ||
| 198 | } finally { | ||
| 199 | updatingSelection = false; | ||
| 200 | } | ||
| 201 | } | ||
| 202 | |||
| 203 | private void refreshUserDetail(UserData actor) { | ||
| 204 | if (actor == null) { | ||
| 205 | userDetailCard.setVisible(false); | ||
| 206 | return; | ||
| 207 | } | ||
| 208 | userDetailCard.setVisible(true); | ||
| 209 | |||
| 210 | userNameLabel.setText(actor.name); | ||
| 211 | userRolePill.setText(actor.blocked ? "BLOCKED" : actor.isMm ? "MARKET MAKER" : "TRADER"); | ||
| 212 | userRolePill.pseudoClassStateChanged(BLOCKED, actor.blocked); | ||
| 213 | userRolePill.pseudoClassStateChanged(MM, !actor.blocked && actor.isMm); | ||
| 214 | userRolePill.pseudoClassStateChanged(TRADER, !actor.blocked && !actor.isMm); | ||
| 215 | |||
| 216 | userSubLabel.setText("Active in " + actor.events.size() + " events"); | ||
| 217 | userBalanceLabel.setText(Format.money(actor.balance)); | ||
| 218 | userBalanceLabel.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0); | ||
| 219 | sparkline.node.pseudoClassStateChanged(NEGATIVE, actor.balance.signum() < 0); | ||
| 220 | sparkline.setPlaying(state.isAnimationsOn()); | ||
| 221 | |||
| 222 | // Blocked Banner | ||
| 223 | blockedBanner.setVisible(actor.blocked); | ||
| 224 | blockedBanner.setManaged(actor.blocked); | ||
| 225 | |||
| 226 | // Balance Chart | ||
| 227 | Charts.setSingleSeries(balanceChart, "balance", actor.balanceHistory); | ||
| 228 | if (balanceChart.getYAxis() instanceof NumberAxis yAxis) { | ||
| 229 | Charts.scaleYAxis(yAxis, actor.balanceHistory, actor.balance.doubleValue()); | ||
| 230 | } | ||
| 231 | |||
| 232 | // Participation Table | ||
| 233 | var evRows = new ArrayList<Node>(actor.events.size()); | ||
| 234 | for (var ev : actor.events) { | ||
| 235 | evRows.add(buildParticipationRow(ev)); | ||
| 236 | } | ||
| 237 | participationRowsContainer.getChildren().setAll(evRows); | ||
| 238 | |||
| 239 | // Trade Panel | ||
| 240 | refreshTradePanel(actor); | ||
| 241 | } | ||
| 242 | |||
| 243 | private void refreshTradePanel(UserData actor) { | ||
| 244 | if (state == null) return; | ||
| 245 | var e = state.selectedEvent(); | ||
| 246 | |||
| 247 | if (e == null) { | ||
| 248 | noTradeEventLabel.setVisible(true); | ||
| 249 | noTradeEventLabel.setManaged(true); | ||
| 250 | tradeContentBox.setVisible(false); | ||
| 251 | tradeContentBox.setManaged(false); | ||
| 252 | return; | ||
| 253 | } | ||
| 254 | |||
| 255 | noTradeEventLabel.setVisible(false); | ||
| 256 | noTradeEventLabel.setManaged(false); | ||
| 257 | tradeContentBox.setVisible(true); | ||
| 258 | tradeContentBox.setManaged(true); | ||
| 259 | |||
| 260 | tradeEventTitle.setText(e.num + ". " + e.name); | ||
| 261 | tradeEventSub.setText( | ||
| 262 | (e.type == MechanismType.LMSR ? "LMSR" : "Order Book") | ||
| 263 | + "  Status: " | ||
| 264 | + e.status | ||
| 265 | + "  Fee: " | ||
| 266 | + e.feeText()); | ||
| 267 | |||
| 268 | int yesHeld = 0, noHeld = 0; | ||
| 269 | if (actor != null && actor.events != null) { | ||
| 270 | for (var ev : actor.events) { | ||
| 271 | if (ev.eventId().equals(e.id)) { | ||
| 272 | yesHeld = ev.yes(); | ||
| 273 | noHeld = ev.no(); | ||
| 274 | break; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | tradeYesPriceLabel.setText(Format.money(e.yesPrice)); | ||
| 280 | tradeYesHeldLabel.setText("You hold " + yesHeld + " shares"); | ||
| 281 | |||
| 282 | tradeNoPriceLabel.setText(Format.money(e.noPrice)); | ||
| 283 | tradeNoHeldLabel.setText("You hold " + noHeld + " shares"); | ||
| 284 | |||
| 285 | Views.setSelected(tradeYesOptCard, state.isTradeOptionYes()); | ||
| 286 | Views.setSelected(tradeNoOptCard, !state.isTradeOptionYes()); | ||
| 287 | |||
| 288 | Views.setSelected(tradeBuyBtn, state.getTradeSide() == TradeSide.BUY); | ||
| 289 | Views.setSelected(tradeSellBtn, state.getTradeSide() == TradeSide.SELL); | ||
| 290 | |||
| 291 | if (tradeQtyField.getText().isEmpty()) tradeQtyField.setText(state.getTradeQty()); | ||
| 292 | if (tradePriceField.getText().isEmpty()) tradePriceField.setText(state.getTradePrice()); | ||
| 293 | |||
| 294 | tradePriceField.setVisible(e.type == MechanismType.ORDER_BOOK); | ||
| 295 | tradePriceField.setManaged(e.type == MechanismType.ORDER_BOOK); | ||
| 296 | |||
| 297 | tradeSubmitBtn.setText( | ||
| 298 | state.getTradeSide() == TradeSide.BUY ? "Place Buy Order" : "Place Sell Order"); | ||
| 299 | |||
| 300 | validateTradeInputs(); | ||
| 301 | } | ||
| 302 | |||
| 303 | private boolean validateTradeInputs() { | ||
| 304 | if (state == null) return false; | ||
| 305 | var e = state.selectedEvent(); | ||
| 306 | var actor = state.actingUser(); | ||
| 307 | if (e == null || actor == null) return false; | ||
| 308 | |||
| 309 | boolean valid = true; | ||
| 310 | String qtyText = tradeQtyField.getText() != null ? tradeQtyField.getText().trim() : ""; | ||
| 311 | long qty = 0; | ||
| 312 | if (qtyText.isEmpty()) { | ||
| 313 | valid = false; | ||
| 314 | tradeQtyField.pseudoClassStateChanged(ERROR, true); | ||
| 315 | tradeHintLabel.setText("Please enter the number of shares."); | ||
| 316 | } else { | ||
| 317 | try { | ||
| 318 | qty = Long.parseLong(qtyText); | ||
| 319 | if (qty <= 0) { | ||
| 320 | valid = false; | ||
| 321 | tradeQtyField.pseudoClassStateChanged(ERROR, true); | ||
| 322 | tradeHintLabel.setText("Shares must be a positive number."); | ||
| 323 | } else { | ||
| 324 | tradeQtyField.pseudoClassStateChanged(ERROR, false); | ||
| 325 | } | ||
| 326 | } catch (NumberFormatException nfe) { | ||
| 327 | valid = false; | ||
| 328 | tradeQtyField.pseudoClassStateChanged(ERROR, true); | ||
| 329 | tradeHintLabel.setText("Shares must be a valid integer."); | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | BigDecimal price = null; | ||
| 334 | if (e.type == MechanismType.ORDER_BOOK) { | ||
| 335 | String priceText = tradePriceField.getText() != null ? tradePriceField.getText().trim() : ""; | ||
| 336 | if (priceText.isEmpty()) { | ||
| 337 | valid = false; | ||
| 338 | tradePriceField.pseudoClassStateChanged(ERROR, true); | ||
| 339 | if (valid) { | ||
| 340 | tradeHintLabel.setText("Please enter a price per share."); | ||
| 341 | } | ||
| 342 | } else { | ||
| 343 | try { | ||
| 344 | price = new BigDecimal(priceText); | ||
| 345 | if (price.compareTo(BigDecimal.ZERO) <= 0) { | ||
| 346 | valid = false; | ||
| 347 | tradePriceField.pseudoClassStateChanged(ERROR, true); | ||
| 348 | tradeHintLabel.setText("Price per share must be positive."); | ||
| 349 | } else { | ||
| 350 | tradePriceField.pseudoClassStateChanged(ERROR, false); | ||
| 351 | } | ||
| 352 | } catch (Exception nfe) { | ||
| 353 | valid = false; | ||
| 354 | tradePriceField.pseudoClassStateChanged(ERROR, true); | ||
| 355 | tradeHintLabel.setText("Price must be a valid decimal number."); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | } else { | ||
| 359 | tradePriceField.pseudoClassStateChanged(ERROR, false); | ||
| 360 | price = state.isTradeOptionYes() ? e.yesPrice : e.noPrice; | ||
| 361 | } | ||
| 362 | |||
| 363 | if (valid && qty > 0 && price != null) { | ||
| 364 | BigDecimal cost = price.multiply(BigDecimal.valueOf(qty)); | ||
| 365 | tradeCostLabel.setText(Format.money(cost)); | ||
| 366 | if (actor.blocked) { | ||
| 367 | tradeHintLabel.setText("Trading is disabled because this account is blocked."); | ||
| 368 | tradeSubmitBtn.setDisable(true); | ||
| 369 | } else if (e.status != EventStatus.ACTIVE) { | ||
| 370 | tradeHintLabel.setText("Trading is only available for ACTIVE events."); | ||
| 371 | tradeSubmitBtn.setDisable(true); | ||
| 372 | } else if (state.getTradeSide() == TradeSide.SELL) { | ||
| 373 | int held = 0; | ||
| 374 | if (actor.events != null) { | ||
| 375 | for (var evRow : actor.events) { | ||
| 376 | if (evRow.eventId().equals(e.id)) { | ||
| 377 | held = state.isTradeOptionYes() ? evRow.yes() : evRow.no(); | ||
| 378 | break; | ||
| 379 | } | ||
| 380 | } | ||
| 381 | } | ||
| 382 | if (!actor.isMm && qty > held) { | ||
| 383 | tradeHintLabel.setText("Cannot sell " + qty + " shares; you only hold " + held + "."); | ||
| 384 | tradeSubmitBtn.setDisable(true); | ||
| 385 | } else { | ||
| 386 | tradeHintLabel.setText("Sell order will match existing bids or rest as an ask."); | ||
| 387 | tradeSubmitBtn.setDisable(false); | ||
| 388 | } | ||
| 389 | } else { | ||
| 390 | if (actor.balance.compareTo(cost) < 0) { | ||
| 391 | tradeHintLabel.setText( | ||
| 392 | "Insufficient balance. Required: " | ||
| 393 | + Format.money(cost) | ||
| 394 | + ", available: " | ||
| 395 | + Format.money(actor.balance)); | ||
| 396 | tradeSubmitBtn.setDisable(true); | ||
| 397 | } else { | ||
| 398 | tradeHintLabel.setText( | ||
| 399 | e.type == MechanismType.ORDER_BOOK | ||
| 400 | ? "Buy order will match existing asks or rest as a bid." | ||
| 401 | : "Orders execute immediately against the market maker."); | ||
| 402 | tradeSubmitBtn.setDisable(false); | ||
| 403 | } | ||
| 404 | } | ||
| 405 | } else { | ||
| 406 | tradeCostLabel.setText("—"); | ||
| 407 | tradeSubmitBtn.setDisable(true); | ||
| 408 | } | ||
| 409 | |||
| 410 | return valid; | ||
| 411 | } | ||
| 412 | |||
| 413 | private Node buildParticipationRow(UserEventRow ev) { | ||
| 414 | var row = new UserEventItemView(ev); | ||
| 415 | row.setOnMouseClicked( | ||
| 416 | e -> { | ||
| 417 | if (state != null) { | ||
| 418 | state.setSelectedEventId(ev.eventId()); | ||
| 419 | } | ||
| 420 | }); | ||
| 421 | return row; | ||
| 422 | } | ||
| 423 | |||
| 424 | // ---- FXML Handlers ---------------------------------------------------- | ||
| 425 | |||
| 426 | @FXML | ||
| 427 | private void handleCreateNewEvent() { | ||
| 428 | if (onCreateNewEvent != null) { | ||
| 429 | onCreateNewEvent.run(); | ||
| 430 | } | ||
| 431 | } | ||
| 432 | |||
| 433 | @FXML | ||
| 434 | private void handleSelectTradeYes() { | ||
| 435 | if (state != null) state.setTradeOptionYes(true); | ||
| 436 | } | ||
| 437 | |||
| 438 | @FXML | ||
| 439 | private void handleSelectTradeNo() { | ||
| 440 | if (state != null) state.setTradeOptionYes(false); | ||
| 441 | } | ||
| 442 | |||
| 443 | @FXML | ||
| 444 | private void handleTradeBuy() { | ||
| 445 | if (state != null) state.setTradeSide(TradeSide.BUY); | ||
| 446 | } | ||
| 447 | |||
| 448 | @FXML | ||
| 449 | private void handleTradeSell() { | ||
| 450 | if (state != null) state.setTradeSide(TradeSide.SELL); | ||
| 451 | } | ||
| 452 | |||
| 453 | @FXML | ||
| 454 | private void handleTradeSubmit() { | ||
| 455 | if (state == null) return; | ||
| 456 | var e = state.selectedEvent(); | ||
| 457 | var u = state.actingUser(); | ||
| 458 | if (e == null || u == null) return; | ||
| 459 | if (!validateTradeInputs()) return; | ||
| 460 | |||
| 461 | long qty = Long.parseLong(tradeQtyField.getText().trim()); | ||
| 462 | BigDecimal price = | ||
| 463 | (e.type == MechanismType.ORDER_BOOK) | ||
| 464 | ? new BigDecimal(tradePriceField.getText().trim()) | ||
| 465 | : (state.isTradeOptionYes() ? e.yesPrice : e.noPrice); | ||
| 466 | |||
| 467 | String optionKey = state.isTradeOptionYes() ? "YES" : "NO"; | ||
| 468 | String side = state.getTradeSide() == TradeSide.SELL ? "SELL" : "BUY"; | ||
| 469 | |||
| 470 | if (onPlaceOrder != null) { | ||
| 471 | onPlaceOrder.placeOrder(e.id, optionKey, side, price, qty); | ||
| 472 | } else if (onToast != null) { | ||
| 473 | onToast.accept("Order placed: " + side + " " + qty + " " + optionKey + " on #" + e.num); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java new file mode 100644 index 0000000..8162419 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/BookRow.java | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | public record BookRow(String side, String user, int qty, String price, boolean isBid) {} | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java new file mode 100644 index 0000000..9dd845c --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ChartPoint.java | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | public record ChartPoint(double x, double y) {} | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java new file mode 100644 index 0000000..f3d0bc4 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/EventData.java | |||
| @@ -0,0 +1,81 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.util.ArrayList; | ||
| 5 | import java.util.List; | ||
| 6 | import market.guess.model.event.CommissionTiming; | ||
| 7 | import market.guess.model.event.EventStatus; | ||
| 8 | import market.guess.model.event.MechanismType; | ||
| 9 | |||
| 10 | public final class EventData { | ||
| 11 | public final String id; | ||
| 12 | |||
| 13 | public final int num; | ||
| 14 | |||
| 15 | public final String name; | ||
| 16 | public final String desc; | ||
| 17 | public final MechanismType type; | ||
| 18 | public EventStatus status; | ||
| 19 | public final String mm; | ||
| 20 | public final int feePercent; | ||
| 21 | public final CommissionTiming feeMode; | ||
| 22 | public BigDecimal contract; | ||
| 23 | public final Integer liquidityB; // LMSR only | ||
| 24 | public final BigDecimal baseValueD; // Order Book only | ||
| 25 | public BigDecimal yesPrice = new BigDecimal(0.5); | ||
| 26 | public BigDecimal noPrice = new BigDecimal(0.5); | ||
| 27 | public int yesShares, noShares; | ||
| 28 | public final List<TradeRow> trades = new ArrayList<>(); | ||
| 29 | public final List<ChartPoint> chart = new ArrayList<>(); | ||
| 30 | public OrderBook yesBook, noBook; | ||
| 31 | public final List<ParticipantRow> participants = new ArrayList<>(); | ||
| 32 | public String resolvedOption; // null unless CLOSED | ||
| 33 | |||
| 34 | public EventData( | ||
| 35 | final String id, | ||
| 36 | final int num, | ||
| 37 | final String name, | ||
| 38 | final String desc, | ||
| 39 | final MechanismType type, | ||
| 40 | final EventStatus status, | ||
| 41 | final String mm, | ||
| 42 | final int feePercent, | ||
| 43 | final CommissionTiming feeMode, | ||
| 44 | final double contract, | ||
| 45 | final Integer liquidityB, | ||
| 46 | final BigDecimal baseValueD) { | ||
| 47 | this.id = id; | ||
| 48 | this.num = num; | ||
| 49 | this.name = name; | ||
| 50 | this.desc = desc; | ||
| 51 | this.type = type; | ||
| 52 | this.status = status; | ||
| 53 | this.mm = mm; | ||
| 54 | this.feePercent = feePercent; | ||
| 55 | this.feeMode = feeMode; | ||
| 56 | this.contract = BigDecimal.valueOf(contract); | ||
| 57 | this.liquidityB = liquidityB; | ||
| 58 | this.baseValueD = baseValueD; | ||
| 59 | } | ||
| 60 | |||
| 61 | public String getId() { | ||
| 62 | return id; | ||
| 63 | } | ||
| 64 | |||
| 65 | public String feeText() { | ||
| 66 | return feePercent + "% on " + (feeMode == CommissionTiming.ON_PURCHASE ? "purchase" : "close"); | ||
| 67 | } | ||
| 68 | |||
| 69 | @Override | ||
| 70 | public boolean equals(Object o) { | ||
| 71 | if (this == o) return true; | ||
| 72 | if (o == null || getClass() != o.getClass()) return false; | ||
| 73 | EventData eventData = (EventData) o; | ||
| 74 | return id != null && id.equals(eventData.id); | ||
| 75 | } | ||
| 76 | |||
| 77 | @Override | ||
| 78 | public int hashCode() { | ||
| 79 | return id != null ? id.hashCode() : 0; | ||
| 80 | } | ||
| 81 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java new file mode 100644 index 0000000..230798d --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/OrderBook.java | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | import java.util.ArrayList; | ||
| 4 | import java.util.List; | ||
| 5 | |||
| 6 | public final class OrderBook { | ||
| 7 | public final String optionName; | ||
| 8 | public final boolean yesOption; | ||
| 9 | public String last, bid, ask, mid, spread; | ||
| 10 | public final List<BookRow> rows = new ArrayList<>(); | ||
| 11 | |||
| 12 | public OrderBook(String optionName, boolean yesOption) { | ||
| 13 | this.optionName = optionName; | ||
| 14 | this.yesOption = yesOption; | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java new file mode 100644 index 0000000..256a974 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/ParticipantRow.java | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | public record ParticipantRow( | ||
| 4 | String user, String tag, int yes, int no, String value, String fees, String pnl) {} | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java new file mode 100644 index 0000000..d95b2d6 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/TradeRow.java | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | public record TradeRow( | ||
| 4 | int n, String user, String option, boolean yesOption, int shares, String paid) {} | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java new file mode 100644 index 0000000..62ad526 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserData.java | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | import java.util.ArrayList; | ||
| 5 | import java.util.List; | ||
| 6 | |||
| 7 | public final class UserData { | ||
| 8 | public final String name; | ||
| 9 | |||
| 10 | public String getName() { | ||
| 11 | return name; | ||
| 12 | } | ||
| 13 | |||
| 14 | public String role; | ||
| 15 | public BigDecimal balance; | ||
| 16 | public final boolean blocked; | ||
| 17 | public final boolean isMm; | ||
| 18 | public final List<ChartPoint> balanceHistory = new ArrayList<>(); | ||
| 19 | public final List<UserEventRow> events = new ArrayList<>(); | ||
| 20 | |||
| 21 | public UserData(String name, String role, double balance, boolean blocked, boolean isMm) { | ||
| 22 | this.name = name; | ||
| 23 | this.role = role; | ||
| 24 | this.balance = BigDecimal.valueOf(balance); | ||
| 25 | this.blocked = blocked; | ||
| 26 | this.isMm = isMm; | ||
| 27 | } | ||
| 28 | |||
| 29 | @Override | ||
| 30 | public boolean equals(Object o) { | ||
| 31 | if (this == o) return true; | ||
| 32 | if (o == null || getClass() != o.getClass()) return false; | ||
| 33 | UserData userData = (UserData) o; | ||
| 34 | return name != null && name.equalsIgnoreCase(userData.name); | ||
| 35 | } | ||
| 36 | |||
| 37 | @Override | ||
| 38 | public int hashCode() { | ||
| 39 | return name != null ? name.toLowerCase().hashCode() : 0; | ||
| 40 | } | ||
| 41 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java new file mode 100644 index 0000000..7c15530 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/model/UserEventRow.java | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | package market.guess.ui.desktop.model; | ||
| 2 | |||
| 3 | public record UserEventRow( | ||
| 4 | String eventId, String eventName, String role, String type, int yes, int no, String pl) {} | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java b/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java new file mode 100644 index 0000000..e49b0c4 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/task/InitialLoadTask.java | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | package market.guess.ui.desktop.task; | ||
| 2 | |||
| 3 | import java.nio.file.Path; | ||
| 4 | import javafx.concurrent.Task; | ||
| 5 | import market.guess.api.CatalogContext; | ||
| 6 | import market.guess.api.LoadResult; | ||
| 7 | |||
| 8 | public final class InitialLoadTask extends Task<LoadResult> { | ||
| 9 | private final CatalogContext catalog; | ||
| 10 | private final Path path; | ||
| 11 | |||
| 12 | public InitialLoadTask(CatalogContext catalog, Path path) { | ||
| 13 | this.catalog = catalog; | ||
| 14 | this.path = path; | ||
| 15 | } | ||
| 16 | |||
| 17 | @Override | ||
| 18 | protected LoadResult call() throws Exception { | ||
| 19 | updateProgress(0, 100); | ||
| 20 | if (isCancelled()) { | ||
| 21 | return null; | ||
| 22 | } | ||
| 23 | |||
| 24 | for (int i = 0; i < 20; i++) { | ||
| 25 | if (isCancelled()) { | ||
| 26 | return null; | ||
| 27 | } | ||
| 28 | try { | ||
| 29 | Thread.sleep(100); | ||
| 30 | } catch (InterruptedException e) { | ||
| 31 | if (isCancelled()) { | ||
| 32 | return null; | ||
| 33 | } | ||
| 34 | Thread.currentThread().interrupt(); | ||
| 35 | break; | ||
| 36 | } | ||
| 37 | updateProgress((i + 1) * 5, 100); | ||
| 38 | } | ||
| 39 | |||
| 40 | if (isCancelled()) { | ||
| 41 | return null; | ||
| 42 | } | ||
| 43 | |||
| 44 | var result = catalog.loadEvents(path); | ||
| 45 | |||
| 46 | if (isCancelled()) { | ||
| 47 | return null; | ||
| 48 | } | ||
| 49 | |||
| 50 | if (!result.isSuccess()) { | ||
| 51 | throw new RuntimeException(result.getDetails()); | ||
| 52 | } | ||
| 53 | |||
| 54 | updateProgress(100, 100); | ||
| 55 | return result.getData(); | ||
| 56 | } | ||
| 57 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java new file mode 100644 index 0000000..54f67ce --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Charts.java | |||
| @@ -0,0 +1,202 @@ | |||
| 1 | package market.guess.ui.desktop.util; | ||
| 2 | |||
| 3 | import java.time.Instant; | ||
| 4 | import java.time.LocalDate; | ||
| 5 | import java.time.ZoneId; | ||
| 6 | import java.time.format.DateTimeFormatter; | ||
| 7 | import java.util.List; | ||
| 8 | import javafx.scene.chart.LineChart; | ||
| 9 | import javafx.scene.chart.NumberAxis; | ||
| 10 | import javafx.scene.chart.XYChart; | ||
| 11 | import javafx.util.StringConverter; | ||
| 12 | import market.guess.ui.desktop.model.ChartPoint; | ||
| 13 | |||
| 14 | /** Utilities for populating LineChart series and formatting time/value axes. */ | ||
| 15 | public final class Charts { | ||
| 16 | private Charts() {} | ||
| 17 | |||
| 18 | public static final long BASE_TIME = | ||
| 19 | LocalDate.of(2026, 9, 12).atTime(9, 0).atZone(ZoneId.systemDefault()).toEpochSecond(); | ||
| 20 | |||
| 21 | public static double toEpochSecond(double x) { | ||
| 22 | if (x >= 1_000_000_000) { | ||
| 23 | return x; | ||
| 24 | } | ||
| 25 | return BASE_TIME + (long) (x * 1800); | ||
| 26 | } | ||
| 27 | |||
| 28 | public static void setSingleSeries( | ||
| 29 | LineChart<Number, Number> chart, String name, List<ChartPoint> pts) { | ||
| 30 | configureTimeXAxis(chart, pts); | ||
| 31 | chart.getData().setAll(List.of(series(name, pts))); | ||
| 32 | } | ||
| 33 | |||
| 34 | public static void setDualSeries( | ||
| 35 | LineChart<Number, Number> chart, | ||
| 36 | String name1, | ||
| 37 | List<ChartPoint> pts1, | ||
| 38 | String name2, | ||
| 39 | List<ChartPoint> pts2) { | ||
| 40 | configureTimeXAxis(chart, (pts1 != null && !pts1.isEmpty()) ? pts1 : pts2); | ||
| 41 | chart.getData().setAll(List.of(series(name1, pts1), series(name2, pts2))); | ||
| 42 | } | ||
| 43 | |||
| 44 | public static void configureTimeXAxis(LineChart<Number, Number> chart, List<ChartPoint> pts) { | ||
| 45 | if (!(chart.getXAxis() instanceof NumberAxis xAxis)) return; | ||
| 46 | |||
| 47 | xAxis.setTickMarkVisible(true); | ||
| 48 | xAxis.setTickLabelsVisible(true); | ||
| 49 | xAxis.setMinorTickVisible(false); | ||
| 50 | xAxis.setForceZeroInRange(false); | ||
| 51 | |||
| 52 | if (pts == null || pts.isEmpty()) { | ||
| 53 | xAxis.setAutoRanging(true); | ||
| 54 | return; | ||
| 55 | } | ||
| 56 | |||
| 57 | double min = pts.stream().mapToDouble(p -> toEpochSecond(p.x())).min().orElse(BASE_TIME); | ||
| 58 | double max = pts.stream().mapToDouble(p -> toEpochSecond(p.x())).max().orElse(BASE_TIME + 3600); | ||
| 59 | |||
| 60 | if (min == max) { | ||
| 61 | min -= 60; | ||
| 62 | max += 60; | ||
| 63 | } | ||
| 64 | |||
| 65 | double range = max - min; | ||
| 66 | double targetInterval = range / 5.5; | ||
| 67 | |||
| 68 | double[] cleanIntervals = { | ||
| 69 | 1, 5, 10, 15, 30, 60, 300, 600, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400 | ||
| 70 | }; | ||
| 71 | double tickUnit = cleanIntervals[cleanIntervals.length - 1]; | ||
| 72 | for (double unit : cleanIntervals) { | ||
| 73 | if (unit >= targetInterval) { | ||
| 74 | tickUnit = unit; | ||
| 75 | break; | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | double lower = Math.floor(min / tickUnit) * tickUnit; | ||
| 80 | double upper = Math.ceil(max / tickUnit) * tickUnit; | ||
| 81 | if (upper <= max) { | ||
| 82 | upper += tickUnit; | ||
| 83 | } | ||
| 84 | |||
| 85 | xAxis.setAutoRanging(false); | ||
| 86 | xAxis.setLowerBound(lower); | ||
| 87 | xAxis.setUpperBound(upper); | ||
| 88 | xAxis.setTickUnit(tickUnit); | ||
| 89 | |||
| 90 | DateTimeFormatter fmt = | ||
| 91 | (range > 86400) | ||
| 92 | ? DateTimeFormatter.ofPattern("MM-dd HH:mm").withZone(ZoneId.systemDefault()) | ||
| 93 | : (range < 300) | ||
| 94 | ? DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()) | ||
| 95 | : DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault()); | ||
| 96 | |||
| 97 | xAxis.setTickLabelFormatter( | ||
| 98 | new StringConverter<Number>() { | ||
| 99 | @Override | ||
| 100 | public String toString(Number n) { | ||
| 101 | if (n == null) return ""; | ||
| 102 | try { | ||
| 103 | return fmt.format(Instant.ofEpochSecond(n.longValue())); | ||
| 104 | } catch (Exception e) { | ||
| 105 | return n.toString(); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | @Override | ||
| 110 | public Number fromString(String s) { | ||
| 111 | return 0; | ||
| 112 | } | ||
| 113 | }); | ||
| 114 | } | ||
| 115 | |||
| 116 | /** | ||
| 117 | * Scales the Y axis dynamically with sensible headroom and round tick marks so values (like | ||
| 118 | * balances > $1,500) never get cut off at the top or bottom. | ||
| 119 | */ | ||
| 120 | public static void scaleYAxis(NumberAxis yAxis, List<ChartPoint> pts, Double currentVal) { | ||
| 121 | if (yAxis == null) return; | ||
| 122 | if ((pts == null || pts.isEmpty()) && currentVal == null) { | ||
| 123 | yAxis.setAutoRanging(true); | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | |||
| 127 | double min = | ||
| 128 | (pts != null && !pts.isEmpty()) | ||
| 129 | ? pts.stream().mapToDouble(ChartPoint::y).min().orElse(0.0) | ||
| 130 | : (currentVal != null ? currentVal : 0.0); | ||
| 131 | double max = | ||
| 132 | (pts != null && !pts.isEmpty()) | ||
| 133 | ? pts.stream().mapToDouble(ChartPoint::y).max().orElse(100.0) | ||
| 134 | : (currentVal != null ? currentVal : 100.0); | ||
| 135 | |||
| 136 | if (currentVal != null) { | ||
| 137 | min = Math.min(min, currentVal); | ||
| 138 | max = Math.max(max, currentVal); | ||
| 139 | } | ||
| 140 | |||
| 141 | if (max <= 0 && min <= 0) { | ||
| 142 | double absMax = Math.abs(min); | ||
| 143 | double unit = niceNum(absMax / 4.0, true); | ||
| 144 | if (unit <= 0) unit = 50; | ||
| 145 | double lower = Math.floor(min / unit) * unit - unit; | ||
| 146 | yAxis.setAutoRanging(false); | ||
| 147 | yAxis.setLowerBound(lower); | ||
| 148 | yAxis.setUpperBound(0.0); | ||
| 149 | yAxis.setTickUnit(unit); | ||
| 150 | return; | ||
| 151 | } | ||
| 152 | |||
| 153 | double targetMax = max > 0 ? max * 1.15 : 100.0; | ||
| 154 | double targetMin = min < 0 ? min * 1.20 : 0.0; | ||
| 155 | double range = targetMax - targetMin; | ||
| 156 | double unit = niceNum(range / 5.0, true); | ||
| 157 | if (unit <= 0) unit = 50; | ||
| 158 | |||
| 159 | double lower = targetMin < 0 ? Math.floor(targetMin / unit) * unit : 0.0; | ||
| 160 | double upper = Math.ceil(targetMax / unit) * unit; | ||
| 161 | if (upper <= max) { | ||
| 162 | upper += unit; | ||
| 163 | } | ||
| 164 | |||
| 165 | yAxis.setAutoRanging(false); | ||
| 166 | yAxis.setLowerBound(lower); | ||
| 167 | yAxis.setUpperBound(upper); | ||
| 168 | yAxis.setTickUnit(unit); | ||
| 169 | } | ||
| 170 | |||
| 171 | private static double niceNum(double range, boolean round) { | ||
| 172 | if (range <= 0) return 1.0; | ||
| 173 | double exponent = Math.floor(Math.log10(range)); | ||
| 174 | double fraction = range / Math.pow(10, exponent); | ||
| 175 | double niceFraction; | ||
| 176 | |||
| 177 | if (round) { | ||
| 178 | if (fraction < 1.5) niceFraction = 1.0; | ||
| 179 | else if (fraction < 3.0) niceFraction = 2.0; | ||
| 180 | else if (fraction < 7.0) niceFraction = 5.0; | ||
| 181 | else niceFraction = 10.0; | ||
| 182 | } else { | ||
| 183 | if (fraction <= 1.0) niceFraction = 1.0; | ||
| 184 | else if (fraction <= 2.0) niceFraction = 2.0; | ||
| 185 | else if (fraction <= 5.0) niceFraction = 5.0; | ||
| 186 | else niceFraction = 10.0; | ||
| 187 | } | ||
| 188 | |||
| 189 | return niceFraction * Math.pow(10, exponent); | ||
| 190 | } | ||
| 191 | |||
| 192 | private static XYChart.Series<Number, Number> series(String name, List<ChartPoint> pts) { | ||
| 193 | var s = new XYChart.Series<Number, Number>(); | ||
| 194 | s.setName(name); | ||
| 195 | if (pts != null) { | ||
| 196 | for (var p : pts) { | ||
| 197 | s.getData().add(new XYChart.Data<>(toEpochSecond(p.x()), p.y())); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | return s; | ||
| 201 | } | ||
| 202 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java new file mode 100644 index 0000000..02380bc --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Format.java | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | package market.guess.ui.desktop.util; | ||
| 2 | |||
| 3 | import java.math.BigDecimal; | ||
| 4 | |||
| 5 | /** Pure formatting helpers for monetary amounts and probabilities. */ | ||
| 6 | public final class Format { | ||
| 7 | private Format() {} | ||
| 8 | |||
| 9 | public static String money(BigDecimal n) { | ||
| 10 | if (n == null) return "$0.00"; | ||
| 11 | return (n.signum() < 0 ? "-$" : "$") + String.format("%.2f", n.abs()); | ||
| 12 | } | ||
| 13 | |||
| 14 | public static String money(double n) { | ||
| 15 | return money(BigDecimal.valueOf(n)); | ||
| 16 | } | ||
| 17 | |||
| 18 | public static String prob(double n) { | ||
| 19 | return String.format("%.2f", n); | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java new file mode 100644 index 0000000..50d9c36 --- /dev/null +++ b/ui-desktop/src/main/java/market/guess/ui/desktop/util/Views.java | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | package market.guess.ui.desktop.util; | ||
| 2 | |||
| 3 | import java.io.IOException; | ||
| 4 | import java.io.UncheckedIOException; | ||
| 5 | import java.net.URL; | ||
| 6 | import javafx.css.PseudoClass; | ||
| 7 | import javafx.fxml.FXMLLoader; | ||
| 8 | import javafx.scene.Node; | ||
| 9 | import javafx.scene.Parent; | ||
| 10 | import market.guess.ui.desktop.AppView; | ||
| 11 | |||
| 12 | /** Shared JavaFX view mechanics, pseudo-class management, and FXML root loading. */ | ||
| 13 | public final class Views { | ||
| 14 | private Views() {} | ||
| 15 | |||
| 16 | public static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); | ||
| 17 | |||
| 18 | public static void setSelected(Node node, boolean selected) { | ||
| 19 | if (node != null) { | ||
| 20 | node.pseudoClassStateChanged(SELECTED, selected); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | public static void loadRoot(Parent root, String fxmlName) { | ||
| 25 | URL res = AppView.class.getResource(fxmlName); | ||
| 26 | if (res == null) { | ||
| 27 | res = AppView.class.getResource("/market/guess/ui/desktop/" + fxmlName); | ||
| 28 | } | ||
| 29 | if (res == null) { | ||
| 30 | throw new IllegalArgumentException("Cannot find FXML resource: " + fxmlName); | ||
| 31 | } | ||
| 32 | FXMLLoader loader = new FXMLLoader(res); | ||
| 33 | loader.setRoot(root); | ||
| 34 | loader.setController(root); | ||
| 35 | try { | ||
| 36 | loader.load(); | ||
| 37 | } catch (IOException e) { | ||
| 38 | throw new UncheckedIOException("Failed to load " + fxmlName, e); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/app_view.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/app_view.fxml new file mode 100644 index 0000000..855b787 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/app_view.fxml | |||
| @@ -0,0 +1,158 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Button?> | ||
| 5 | <?import javafx.scene.control.ComboBox?> | ||
| 6 | <?import javafx.scene.control.Label?> | ||
| 7 | <?import javafx.scene.control.ScrollPane?> | ||
| 8 | <?import javafx.scene.control.Tab?> | ||
| 9 | <?import javafx.scene.control.TabPane?> | ||
| 10 | <?import javafx.scene.control.Tooltip?> | ||
| 11 | <?import javafx.scene.layout.BorderPane?> | ||
| 12 | <?import javafx.scene.layout.HBox?> | ||
| 13 | <?import javafx.scene.layout.Region?> | ||
| 14 | <?import javafx.scene.layout.StackPane?> | ||
| 15 | <?import javafx.scene.layout.VBox?> | ||
| 16 | <?import javafx.scene.shape.SVGPath?> | ||
| 17 | |||
| 18 | <StackPane fx:id="rootStack" styleClass="gm-root" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.AppController"> | ||
| 19 | <children> | ||
| 20 | <!-- Safety net: scrolls only if the viewport gets smaller than AppView.MIN_WIDTH x MIN_HEIGHT. --> | ||
| 21 | <ScrollPane fitToHeight="true" fitToWidth="true" styleClass="gm-app-scroll"> | ||
| 22 | <content> | ||
| 23 | <BorderPane fx:id="chrome" styleClass="gm-chrome"> | ||
| 24 | <top> | ||
| 25 | <VBox> | ||
| 26 | <children> | ||
| 27 | <!-- Window Bar --> | ||
| 28 | <HBox fx:id="windowBar" alignment="CENTER_LEFT" prefHeight="32.0" spacing="10.0" styleClass="gm-bg-panel2, gm-divider-bottom"> | ||
| 29 | <padding> | ||
| 30 | <Insets left="14.0" right="14.0" /> | ||
| 31 | </padding> | ||
| 32 | <children> | ||
| 33 | <HBox alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-traffic-lights"> | ||
| 34 | <children> | ||
| 35 | <StackPane maxHeight="12.0" maxWidth="12.0" minHeight="12.0" minWidth="12.0" onMouseClicked="#handleClose" prefHeight="12.0" prefWidth="12.0" styleClass="gm-traffic-dot, gm-traffic-dot-no"> | ||
| 36 | <children> | ||
| 37 | <SVGPath content="M 0 0 L 5 5 M 5 0 L 0 5" styleClass="gm-traffic-icon" /> | ||
| 38 | </children> | ||
| 39 | </StackPane> | ||
| 40 | <StackPane maxHeight="12.0" maxWidth="12.0" minHeight="12.0" minWidth="12.0" onMouseClicked="#handleMinimize" prefHeight="12.0" prefWidth="12.0" styleClass="gm-traffic-dot, gm-traffic-dot-accent"> | ||
| 41 | <children> | ||
| 42 | <SVGPath content="M 0 2.5 L 6 2.5" styleClass="gm-traffic-icon" /> | ||
| 43 | </children> | ||
| 44 | </StackPane> | ||
| 45 | <StackPane maxHeight="12.0" maxWidth="12.0" minHeight="12.0" minWidth="12.0" onMouseClicked="#handleMaximize" prefHeight="12.0" prefWidth="12.0" styleClass="gm-traffic-dot, gm-traffic-dot-yes"> | ||
| 46 | <children> | ||
| 47 | <SVGPath content="M 0 2.5 L 5 2.5 M 2.5 0 L 2.5 5" styleClass="gm-traffic-icon" /> | ||
| 48 | </children> | ||
| 49 | </StackPane> | ||
| 50 | </children> | ||
| 51 | </HBox> | ||
| 52 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink2" text="Guess Market" /> | ||
| 53 | </children> | ||
| 54 | </HBox> | ||
| 55 | <!-- Toolbar --> | ||
| 56 | <HBox alignment="CENTER_LEFT" spacing="12.0" styleClass="gm-bg-panel, gm-divider-bottom"> | ||
| 57 | <padding> | ||
| 58 | <Insets bottom="12.0" left="16.0" right="16.0" top="12.0" /> | ||
| 59 | </padding> | ||
| 60 | <children> | ||
| 61 | <Button minWidth="-Infinity" onAction="#handleLoadFile" styleClass="gm-btn, gm-btn-primary" text="Load File…" /> | ||
| 62 | <HBox alignment="CENTER_LEFT" spacing="10.0" styleClass="gm-card-panel2" HBox.hgrow="ALWAYS"> | ||
| 63 | <padding> | ||
| 64 | <Insets bottom="8.0" left="12.0" right="12.0" top="8.0" /> | ||
| 65 | </padding> | ||
| 66 | <children> | ||
| 67 | <Label minWidth="-Infinity" styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="" /> | ||
| 68 | <Label fx:id="loadedFilePathLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-base, gm-font-mono, gm-font-medium, gm-text-ink" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 69 | </children> | ||
| 70 | </HBox> | ||
| 71 | <HBox alignment="CENTER_RIGHT" minWidth="-Infinity" spacing="12.0"> | ||
| 72 | <children> | ||
| 73 | <Button fx:id="animationsButton" onAction="#handleAnimationsToggled" styleClass="gm-btn, gm-btn-secondary, gm-btn-icon"> | ||
| 74 | <tooltip> | ||
| 75 | <Tooltip text="Toggle animations" /> | ||
| 76 | </tooltip> | ||
| 77 | </Button> | ||
| 78 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 79 | <children> | ||
| 80 | <ComboBox fx:id="skinComboBox" onAction="#handleSkinChanged" styleClass="gm-select-base, gm-select-btn-bg" /> | ||
| 81 | </children> | ||
| 82 | </HBox> | ||
| 83 | </children> | ||
| 84 | </HBox> | ||
| 85 | </children> | ||
| 86 | </HBox> | ||
| 87 | </children> | ||
| 88 | </VBox> | ||
| 89 | </top> | ||
| 90 | <center> | ||
| 91 | <StackPane fx:id="centerContainer"> | ||
| 92 | <children> | ||
| 93 | <fx:include fx:id="emptyState" source="controllers/empty_state.fxml" /> | ||
| 94 | <TabPane fx:id="tabPane" managed="false" styleClass="gm-tabs" tabClosingPolicy="UNAVAILABLE" visible="false"> | ||
| 95 | <tabs> | ||
| 96 | <Tab text="Events"> | ||
| 97 | <content> | ||
| 98 | <fx:include fx:id="eventsTab" source="controllers/events_tab.fxml" /> | ||
| 99 | </content> | ||
| 100 | </Tab> | ||
| 101 | <Tab text="Users"> | ||
| 102 | <content> | ||
| 103 | <fx:include fx:id="usersTab" source="controllers/users_tab.fxml" /> | ||
| 104 | </content> | ||
| 105 | </Tab> | ||
| 106 | </tabs> | ||
| 107 | </TabPane> | ||
| 108 | <!-- Overlaid on the right of the tab header; height is bound to the header in AppController --> | ||
| 109 | <HBox fx:id="tabInfo" alignment="CENTER_RIGHT" maxHeight="-Infinity" pickOnBounds="false" spacing="8.0" visible="false" StackPane.alignment="TOP_RIGHT"> | ||
| 110 | <padding> | ||
| 111 | <Insets left="4.0" right="20.0" /> | ||
| 112 | </padding> | ||
| 113 | <children> | ||
| 114 | <Label fx:id="actingAsLabel" styleClass="gm-text-base, gm-font-medium, gm-text-ink2"> | ||
| 115 | <padding> | ||
| 116 | <Insets right="12.0" /> | ||
| 117 | </padding> | ||
| 118 | </Label> | ||
| 119 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="ACCOUNT BALANCE" /> | ||
| 120 | <Label fx:id="balanceValueLabel" styleClass="gm-font-mono, gm-text-lg, gm-font-semibold, gm-text-accent" /> | ||
| 121 | </children> | ||
| 122 | </HBox> | ||
| 123 | </children> | ||
| 124 | </StackPane> | ||
| 125 | </center> | ||
| 126 | <bottom> | ||
| 127 | <HBox alignment="CENTER_LEFT" prefHeight="30.0" spacing="12.0" styleClass="gm-bg-panel2, gm-divider-top"> | ||
| 128 | <padding> | ||
| 129 | <Insets left="14.0" right="14.0" /> | ||
| 130 | </padding> | ||
| 131 | <children> | ||
| 132 | <Label fx:id="statusBarLabel" styleClass="gm-text-sm, gm-font-mono, gm-text-ink2" /> | ||
| 133 | <Region HBox.hgrow="ALWAYS" /> | ||
| 134 | <Button fx:id="ejectButton" onAction="#handleEjectFile" styleClass="gm-link-button" text="ó°®‘" visible="false" /> | ||
| 135 | </children> | ||
| 136 | </HBox> | ||
| 137 | </bottom> | ||
| 138 | </BorderPane> | ||
| 139 | </content> | ||
| 140 | </ScrollPane> | ||
| 141 | <StackPane fx:id="dialogContainer" mouseTransparent="true" visible="false"> | ||
| 142 | <children> | ||
| 143 | <fx:include fx:id="loadDialog" managed="false" source="controllers/dialog_load.fxml" visible="false" /> | ||
| 144 | <fx:include fx:id="createEventDialog" managed="false" source="controllers/dialog_create_event.fxml" visible="false" /> | ||
| 145 | <fx:include fx:id="resolveDialog" managed="false" source="controllers/dialog_resolve.fxml" visible="false" /> | ||
| 146 | </children> | ||
| 147 | </StackPane> | ||
| 148 | <StackPane fx:id="toastContainer" mouseTransparent="true" pickOnBounds="false"> | ||
| 149 | <children> | ||
| 150 | <Label fx:id="toastLabel" styleClass="gm-toast" visible="false" StackPane.alignment="BOTTOM_CENTER"> | ||
| 151 | <StackPane.margin> | ||
| 152 | <Insets bottom="44.0" /> | ||
| 153 | </StackPane.margin> | ||
| 154 | </Label> | ||
| 155 | </children> | ||
| 156 | </StackPane> | ||
| 157 | </children> | ||
| 158 | </StackPane> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_create_event.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_create_event.fxml new file mode 100644 index 0000000..c8f092f --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_create_event.fxml | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Button?> | ||
| 5 | <?import javafx.scene.control.CheckBox?> | ||
| 6 | <?import javafx.scene.control.ComboBox?> | ||
| 7 | <?import javafx.scene.control.Label?> | ||
| 8 | <?import javafx.scene.control.ScrollPane?> | ||
| 9 | <?import javafx.scene.control.TextArea?> | ||
| 10 | <?import javafx.scene.control.TextField?> | ||
| 11 | <?import javafx.scene.layout.HBox?> | ||
| 12 | <?import javafx.scene.layout.Region?> | ||
| 13 | <?import javafx.scene.layout.StackPane?> | ||
| 14 | <?import javafx.scene.layout.VBox?> | ||
| 15 | |||
| 16 | <StackPane styleClass="gm-modal-scrim" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.CreateEventDialogController"> | ||
| 17 | <children> | ||
| 18 | <ScrollPane fitToWidth="true" maxHeight="640.0" maxWidth="620.0" prefWidth="620.0" styleClass="gm-modal-card" StackPane.alignment="CENTER"> | ||
| 19 | <content> | ||
| 20 | <VBox> | ||
| 21 | <children> | ||
| 22 | <!-- Header --> | ||
| 23 | <HBox alignment="BASELINE_LEFT" styleClass="gm-divider-bottom"> | ||
| 24 | <padding> | ||
| 25 | <Insets bottom="16.0" left="20.0" right="20.0" top="16.0" /> | ||
| 26 | </padding> | ||
| 27 | <children> | ||
| 28 | <Label styleClass="gm-text-xl, gm-font-bold, gm-text-ink" text="Create new event" /> | ||
| 29 | <Region HBox.hgrow="ALWAYS" /> | ||
| 30 | </children> | ||
| 31 | </HBox> | ||
| 32 | <!-- Body Form --> | ||
| 33 | <VBox spacing="14.0"> | ||
| 34 | <padding> | ||
| 35 | <Insets bottom="18.0" left="20.0" right="20.0" top="18.0" /> | ||
| 36 | </padding> | ||
| 37 | <children> | ||
| 38 | <!-- Name Field --> | ||
| 39 | <VBox spacing="5.0"> | ||
| 40 | <children> | ||
| 41 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Event name" /> | ||
| 42 | <TextField fx:id="nameField" maxWidth="1.7976931348623157E308" promptText="Will the summit happen before Q4?" styleClass="gm-textfield" /> | ||
| 43 | </children> | ||
| 44 | </VBox> | ||
| 45 | <!-- Description Field --> | ||
| 46 | <VBox spacing="5.0"> | ||
| 47 | <children> | ||
| 48 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Description & termination condition" /> | ||
| 49 | <TextArea fx:id="descArea" prefRowCount="2" promptText="Resolves YES if a joint summit is publicly confirmed and held before October 1st." styleClass="gm-textarea" wrapText="true" /> | ||
| 50 | </children> | ||
| 51 | </VBox> | ||
| 52 | <!-- Row 1: Trading Method & Fee Collection --> | ||
| 53 | <HBox spacing="12.0"> | ||
| 54 | <children> | ||
| 55 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 56 | <children> | ||
| 57 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Trading method" /> | ||
| 58 | <HBox> | ||
| 59 | <children> | ||
| 60 | <Button fx:id="lmsrBtn" maxWidth="1.7976931348623157E308" onAction="#handleSelectLmsr" styleClass="gm-seg, gm-seg-left, gm-seg-pad-tight" text="LMSR" HBox.hgrow="ALWAYS" /> | ||
| 61 | <Button fx:id="orderBookBtn" maxWidth="1.7976931348623157E308" onAction="#handleSelectOrderBook" styleClass="gm-seg, gm-seg-right, gm-seg-pad-tight" text="Order Book" HBox.hgrow="ALWAYS" /> | ||
| 62 | </children> | ||
| 63 | </HBox> | ||
| 64 | </children> | ||
| 65 | </VBox> | ||
| 66 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 67 | <children> | ||
| 68 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Fee collection" /> | ||
| 69 | <ComboBox fx:id="feeCollectionCombo" maxWidth="1.7976931348623157E308" styleClass="gm-select-base, gm-select-panel2-bg" /> | ||
| 70 | </children> | ||
| 71 | </VBox> | ||
| 72 | </children> | ||
| 73 | </HBox> | ||
| 74 | <!-- Row 2: Fee Percent & Parameter --> | ||
| 75 | <HBox spacing="12.0"> | ||
| 76 | <children> | ||
| 77 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 78 | <children> | ||
| 79 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Fee percent (0–90)" /> | ||
| 80 | <TextField fx:id="feePercentField" maxWidth="1.7976931348623157E308" promptText="5" styleClass="gm-textfield, gm-textfield-mono" /> | ||
| 81 | </children> | ||
| 82 | </VBox> | ||
| 83 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 84 | <children> | ||
| 85 | <Label fx:id="paramLabel" styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Liquidity b (integer)" /> | ||
| 86 | <TextField fx:id="paramField" maxWidth="1.7976931348623157E308" promptText="100" styleClass="gm-textfield, gm-textfield-mono" /> | ||
| 87 | </children> | ||
| 88 | </VBox> | ||
| 89 | </children> | ||
| 90 | </HBox> | ||
| 91 | <!-- Option Names --> | ||
| 92 | <HBox spacing="12.0"> | ||
| 93 | <children> | ||
| 94 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 95 | <children> | ||
| 96 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Option 1" /> | ||
| 97 | <TextField fx:id="opt1Field" maxWidth="1.7976931348623157E308" promptText="YES" styleClass="gm-textfield" /> | ||
| 98 | </children> | ||
| 99 | </VBox> | ||
| 100 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 101 | <children> | ||
| 102 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Option 2" /> | ||
| 103 | <TextField fx:id="opt2Field" maxWidth="1.7976931348623157E308" promptText="NO" styleClass="gm-textfield" /> | ||
| 104 | </children> | ||
| 105 | </VBox> | ||
| 106 | </children> | ||
| 107 | </HBox> | ||
| 108 | <!-- Minting CheckBox (Order Book only) --> | ||
| 109 | <CheckBox fx:id="mintingCheckBox" selected="true" styleClass="gm-checkbox-strong" text="Allow minting new share pairs when bid + ask exceeds d" visible="false" /> | ||
| 110 | <!-- Info Box --> | ||
| 111 | <HBox styleClass="gm-card-panel2"> | ||
| 112 | <padding> | ||
| 113 | <Insets bottom="11.0" left="13.0" right="13.0" top="11.0" /> | ||
| 114 | </padding> | ||
| 115 | <children> | ||
| 116 | <Label fx:id="infoNoteLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" wrapText="true" /> | ||
| 117 | </children> | ||
| 118 | </HBox> | ||
| 119 | </children> | ||
| 120 | </VBox> | ||
| 121 | <!-- Footer --> | ||
| 122 | <HBox alignment="CENTER_LEFT" spacing="9.0" styleClass="gm-divider-top"> | ||
| 123 | <padding> | ||
| 124 | <Insets bottom="14.0" left="20.0" right="20.0" top="14.0" /> | ||
| 125 | </padding> | ||
| 126 | <children> | ||
| 127 | <Region HBox.hgrow="ALWAYS" /> | ||
| 128 | <Button onAction="#handleCancel" styleClass="gm-btn, gm-btn-secondary" text="Cancel" /> | ||
| 129 | <Button onAction="#handleCreate" styleClass="gm-btn, gm-btn-primary" text="Create event" /> | ||
| 130 | </children> | ||
| 131 | </HBox> | ||
| 132 | </children> | ||
| 133 | </VBox> | ||
| 134 | </content> | ||
| 135 | </ScrollPane> | ||
| 136 | </children> | ||
| 137 | </StackPane> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_load.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_load.fxml new file mode 100644 index 0000000..d93ac9e --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_load.fxml | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Button?> | ||
| 5 | <?import javafx.scene.control.Label?> | ||
| 6 | <?import javafx.scene.control.ProgressBar?> | ||
| 7 | <?import javafx.scene.layout.HBox?> | ||
| 8 | <?import javafx.scene.layout.Region?> | ||
| 9 | <?import javafx.scene.layout.StackPane?> | ||
| 10 | <?import javafx.scene.layout.VBox?> | ||
| 11 | |||
| 12 | <StackPane styleClass="gm-modal-scrim" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.LoadDialogController"> | ||
| 13 | <children> | ||
| 14 | <VBox maxHeight="-Infinity" maxWidth="-Infinity" prefWidth="430.0" spacing="12.0" styleClass="gm-card" StackPane.alignment="CENTER"> | ||
| 15 | <padding> | ||
| 16 | <Insets bottom="20.0" left="22.0" right="22.0" top="20.0" /> | ||
| 17 | </padding> | ||
| 18 | <children> | ||
| 19 | <Label styleClass="gm-text-lg, gm-font-bold, gm-text-ink" text="Loading events file…" /> | ||
| 20 | <Label fx:id="pathLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-sm, gm-font-mono, gm-font-medium, gm-text-ink2" textOverrun="ELLIPSIS" /> | ||
| 21 | <ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" prefHeight="9.0" progress="0.06" styleClass="gm-progress, gm-progress-accent" /> | ||
| 22 | <HBox alignment="CENTER_LEFT"> | ||
| 23 | <children> | ||
| 24 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Validating users, market makers and fees…" /> | ||
| 25 | <Region HBox.hgrow="ALWAYS" /> | ||
| 26 | <Button onAction="#handleCancel" styleClass="gm-btn, gm-btn-secondary" text="Cancel" /> | ||
| 27 | </children> | ||
| 28 | </HBox> | ||
| 29 | </children> | ||
| 30 | </VBox> | ||
| 31 | </children> | ||
| 32 | </StackPane> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_resolve.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_resolve.fxml new file mode 100644 index 0000000..b5a9634 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/dialog_resolve.fxml | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Button?> | ||
| 5 | <?import javafx.scene.control.Label?> | ||
| 6 | <?import javafx.scene.layout.HBox?> | ||
| 7 | <?import javafx.scene.layout.Priority?> | ||
| 8 | <?import javafx.scene.layout.Region?> | ||
| 9 | <?import javafx.scene.layout.StackPane?> | ||
| 10 | <?import javafx.scene.layout.VBox?> | ||
| 11 | |||
| 12 | <StackPane styleClass="gm-modal-scrim" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.ResolveDialogController"> | ||
| 13 | <children> | ||
| 14 | <VBox maxHeight="-Infinity" maxWidth="-Infinity" prefWidth="470.0" styleClass="gm-card" StackPane.alignment="CENTER"> | ||
| 15 | <children> | ||
| 16 | <!-- Title --> | ||
| 17 | <HBox alignment="CENTER_LEFT" styleClass="gm-divider-bottom"> | ||
| 18 | <padding> | ||
| 19 | <Insets bottom="16.0" left="20.0" right="20.0" top="16.0" /> | ||
| 20 | </padding> | ||
| 21 | <children> | ||
| 22 | <Label styleClass="gm-text-lg, gm-font-bold, gm-text-ink" text="Close & resolve event" /> | ||
| 23 | </children> | ||
| 24 | </HBox> | ||
| 25 | <!-- Body --> | ||
| 26 | <VBox spacing="13.0"> | ||
| 27 | <padding> | ||
| 28 | <Insets bottom="18.0" left="20.0" right="20.0" top="18.0" /> | ||
| 29 | </padding> | ||
| 30 | <children> | ||
| 31 | <Label styleClass="gm-text-base, gm-font-regular, gm-text-ink2" text="Pick the option the event ended with. The contract account is emptied to holders of the winning option; a close-time fee is moved to the market maker. This cannot be undone." wrapText="true" /> | ||
| 32 | <HBox spacing="10.0"> | ||
| 33 | <children> | ||
| 34 | <Button fx:id="resolveYesBtn" maxWidth="1.7976931348623157E308" onAction="#handleSelectYes" styleClass="gm-resolve-option, gm-resolve-option-yes" text="YES" HBox.hgrow="ALWAYS" /> | ||
| 35 | <Button fx:id="resolveNoBtn" maxWidth="1.7976931348623157E308" onAction="#handleSelectNo" styleClass="gm-resolve-option, gm-resolve-option-no" text="NO" HBox.hgrow="ALWAYS" /> | ||
| 36 | </children> | ||
| 37 | </HBox> | ||
| 38 | </children> | ||
| 39 | </VBox> | ||
| 40 | <!-- Footer --> | ||
| 41 | <HBox alignment="CENTER_LEFT" spacing="9.0" styleClass="gm-divider-top"> | ||
| 42 | <padding> | ||
| 43 | <Insets bottom="14.0" left="20.0" right="20.0" top="14.0" /> | ||
| 44 | </padding> | ||
| 45 | <children> | ||
| 46 | <Region HBox.hgrow="ALWAYS" /> | ||
| 47 | <Button onAction="#handleCancel" styleClass="gm-btn, gm-btn-secondary" text="Cancel" /> | ||
| 48 | <Button fx:id="confirmBtn" disable="true" onAction="#handleConfirm" styleClass="gm-btn, gm-btn-destructive" text="Resolve as …" /> | ||
| 49 | </children> | ||
| 50 | </HBox> | ||
| 51 | </children> | ||
| 52 | </VBox> | ||
| 53 | </children> | ||
| 54 | </StackPane> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/empty_state.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/empty_state.fxml new file mode 100644 index 0000000..0385759 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/empty_state.fxml | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.scene.control.Button?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.StackPane?> | ||
| 6 | <?import javafx.scene.layout.VBox?> | ||
| 7 | <?import market.guess.ui.desktop.components.graphic.MarketChart?> | ||
| 8 | |||
| 9 | <StackPane styleClass="gm-chrome" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.EmptyStateController"> | ||
| 10 | <children> | ||
| 11 | <VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" spacing="14.0"> | ||
| 12 | <children> | ||
| 13 | <MarketChart fx:id="marketChart" /> | ||
| 14 | <Label styleClass="gm-text-xl, gm-font-semibold, gm-text-ink" text="No file loaded" /> | ||
| 15 | <Label alignment="CENTER" maxWidth="420.0" styleClass="gm-text-md, gm-font-regular, gm-text-ink2" text="Load an XML file to populate events and users" textAlignment="CENTER" wrapText="true" /> | ||
| 16 | </children> | ||
| 17 | </VBox> | ||
| 18 | </children> | ||
| 19 | </StackPane> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/events_tab.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/events_tab.fxml new file mode 100644 index 0000000..4e2c1d4 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/events_tab.fxml | |||
| @@ -0,0 +1,448 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.layout.FlowPane?> | ||
| 5 | <?import javafx.scene.chart.LineChart?> | ||
| 6 | <?import javafx.scene.chart.NumberAxis?> | ||
| 7 | <?import javafx.scene.control.Button?> | ||
| 8 | <?import javafx.scene.control.Label?> | ||
| 9 | <?import javafx.scene.control.ListView?> | ||
| 10 | <?import javafx.scene.control.ProgressBar?> | ||
| 11 | <?import javafx.scene.control.ScrollPane?> | ||
| 12 | <?import javafx.scene.layout.HBox?> | ||
| 13 | <?import javafx.scene.layout.Priority?> | ||
| 14 | <?import javafx.scene.layout.Region?> | ||
| 15 | <?import javafx.scene.layout.StackPane?> | ||
| 16 | <?import javafx.scene.layout.VBox?> | ||
| 17 | |||
| 18 | <HBox spacing="12.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.EventsTabController"> | ||
| 19 | <padding> | ||
| 20 | <Insets bottom="12.0" left="16.0" right="16.0" top="12.0" /> | ||
| 21 | </padding> | ||
| 22 | <children> | ||
| 23 | <!-- Left Card: Filters + List --> | ||
| 24 | <VBox maxWidth="498.0" minWidth="498.0" prefWidth="498.0" spacing="10.0" styleClass="gm-card"> | ||
| 25 | <padding> | ||
| 26 | <Insets bottom="12.0" left="12.0" right="12.0" top="12.0" /> | ||
| 27 | </padding> | ||
| 28 | <children> | ||
| 29 | <!-- Method Filter --> | ||
| 30 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 31 | <children> | ||
| 32 | <Label minWidth="74.0" prefWidth="74.0" styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Method" /> | ||
| 33 | <Button fx:id="methodFilterAllBtn" onAction="#handleMethodAll" styleClass="gm-toggle" text="All" /> | ||
| 34 | <Button fx:id="methodFilterLmsrBtn" onAction="#handleMethodLmsr" styleClass="gm-toggle" text="LMSR" /> | ||
| 35 | <Button fx:id="methodFilterObBtn" onAction="#handleMethodOb" styleClass="gm-toggle" text="Order Book" /> | ||
| 36 | </children> | ||
| 37 | </HBox> | ||
| 38 | <!-- Status Filter --> | ||
| 39 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 40 | <children> | ||
| 41 | <Label minWidth="74.0" prefWidth="74.0" styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Status" /> | ||
| 42 | <Button fx:id="statusFilterAllBtn" onAction="#handleStatusAll" styleClass="gm-toggle" text="All" /> | ||
| 43 | <Button fx:id="statusFilterNotStartedBtn" onAction="#handleStatusNotStarted" styleClass="gm-toggle" text="Not started" /> | ||
| 44 | <Button fx:id="statusFilterActiveBtn" onAction="#handleStatusActive" styleClass="gm-toggle" text="Active" /> | ||
| 45 | <Button fx:id="statusFilterClosedBtn" onAction="#handleStatusClosed" styleClass="gm-toggle" text="Closed" /> | ||
| 46 | </children> | ||
| 47 | </HBox> | ||
| 48 | <!-- Fee Filter --> | ||
| 49 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 50 | <children> | ||
| 51 | <Label minWidth="74.0" prefWidth="74.0" styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Fee" /> | ||
| 52 | <Button fx:id="feeFilterAllBtn" onAction="#handleFeeAll" styleClass="gm-toggle" text="All" /> | ||
| 53 | <Button fx:id="feeFilterPurchaseBtn" onAction="#handleFeePurchase" styleClass="gm-toggle" text="On purchase" /> | ||
| 54 | <Button fx:id="feeFilterCloseBtn" onAction="#handleFeeClose" styleClass="gm-toggle" text="On close" /> | ||
| 55 | </children> | ||
| 56 | </HBox> | ||
| 57 | <!-- Virtualized Event List --> | ||
| 58 | <ListView fx:id="eventListContainer" styleClass="gm-scroll-transparent, gm-list-view" VBox.vgrow="ALWAYS" /> | ||
| 59 | <!-- Footer --> | ||
| 60 | <HBox alignment="CENTER_LEFT"> | ||
| 61 | <children> | ||
| 62 | <Label fx:id="eventCountLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="0 of 0 events shown" /> | ||
| 63 | <Region HBox.hgrow="ALWAYS" /> | ||
| 64 | <Button onAction="#handleNewEvent" styleClass="gm-btn, gm-btn-secondary" text="+ New Event" /> | ||
| 65 | </children> | ||
| 66 | </HBox> | ||
| 67 | </children> | ||
| 68 | </VBox> | ||
| 69 | <!-- Right Card: Event Details --> | ||
| 70 | <VBox fx:id="eventDetailCard" styleClass="gm-card" HBox.hgrow="ALWAYS"> | ||
| 71 | <children> | ||
| 72 | <!-- Detail Header --> | ||
| 73 | <VBox fx:id="detailHeaderBox" spacing="9.0" styleClass="gm-divider-bottom"> | ||
| 74 | <padding> | ||
| 75 | <Insets bottom="14.0" left="16.0" right="16.0" top="14.0" /> | ||
| 76 | </padding> | ||
| 77 | <children> | ||
| 78 | <HBox alignment="TOP_LEFT" spacing="12.0"> | ||
| 79 | <children> | ||
| 80 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 81 | <children> | ||
| 82 | <Label fx:id="eventNameLabel" styleClass="gm-text-xl, gm-font-bold, gm-text-ink" /> | ||
| 83 | <Label fx:id="eventDescLabel" styleClass="gm-text-base, gm-font-regular, gm-text-ink2" wrapText="true" /> | ||
| 84 | </children> | ||
| 85 | </VBox> | ||
| 86 | <HBox spacing="7.0"> | ||
| 87 | <children> | ||
| 88 | <Button fx:id="openEventBtn" onAction="#handleOpenEvent" styleClass="gm-btn, gm-btn-primary" text="Open Event" /> | ||
| 89 | <Button fx:id="closeEventBtn" onAction="#handleCloseEvent" styleClass="gm-btn, gm-btn-secondary" text="Close & Resolve…" /> | ||
| 90 | </children> | ||
| 91 | </HBox> | ||
| 92 | </children> | ||
| 93 | </HBox> | ||
| 94 | <HBox fx:id="metaFieldsBox" alignment="CENTER_LEFT" spacing="16.0"> | ||
| 95 | <children> | ||
| 96 | <HBox fx:id="livePill" alignment="CENTER_LEFT" spacing="7.0" styleClass="gm-pill, gm-live-pill"> | ||
| 97 | <children> | ||
| 98 | <StackPane fx:id="liveBarsBox" /> | ||
| 99 | <Label fx:id="liveLabel" styleClass="gm-live-label" /> | ||
| 100 | </children> | ||
| 101 | </HBox> | ||
| 102 | <HBox alignment="CENTER_LEFT" spacing="4.0"> | ||
| 103 | <children> | ||
| 104 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Method" /> | ||
| 105 | <Label fx:id="metaMethodLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 106 | </children> | ||
| 107 | </HBox> | ||
| 108 | <HBox alignment="CENTER_LEFT" spacing="4.0"> | ||
| 109 | <children> | ||
| 110 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Status" /> | ||
| 111 | <Label fx:id="metaStatusLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 112 | </children> | ||
| 113 | </HBox> | ||
| 114 | <HBox alignment="CENTER_LEFT" spacing="4.0"> | ||
| 115 | <children> | ||
| 116 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Market Maker" /> | ||
| 117 | <Label fx:id="metaMmLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 118 | </children> | ||
| 119 | </HBox> | ||
| 120 | <HBox alignment="CENTER_LEFT" spacing="4.0"> | ||
| 121 | <children> | ||
| 122 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Fee" /> | ||
| 123 | <Label fx:id="metaFeeLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 124 | </children> | ||
| 125 | </HBox> | ||
| 126 | <HBox alignment="CENTER_LEFT" spacing="4.0"> | ||
| 127 | <children> | ||
| 128 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Contract" /> | ||
| 129 | <Label fx:id="metaContractLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 130 | </children> | ||
| 131 | </HBox> | ||
| 132 | </children> | ||
| 133 | </HBox> | ||
| 134 | </children> | ||
| 135 | </VBox> | ||
| 136 | <!-- Detail Scrollable Body --> | ||
| 137 | <ScrollPane fitToWidth="true" styleClass="gm-scroll-transparent" VBox.vgrow="ALWAYS"> | ||
| 138 | <content> | ||
| 139 | <VBox spacing="14.0"> | ||
| 140 | <padding> | ||
| 141 | <Insets bottom="14.0" left="16.0" right="16.0" top="14.0" /> | ||
| 142 | </padding> | ||
| 143 | <children> | ||
| 144 | <!-- LMSR Section --> | ||
| 145 | <VBox fx:id="lmsrSection" spacing="14.0"> | ||
| 146 | <children> | ||
| 147 | <!-- YES / NO Option Cards --> | ||
| 148 | <HBox spacing="10.0"> | ||
| 149 | <children> | ||
| 150 | <VBox spacing="9.0" styleClass="gm-card-soft-yes" HBox.hgrow="ALWAYS"> | ||
| 151 | <padding> | ||
| 152 | <Insets bottom="12.0" left="13.0" right="13.0" top="12.0" /> | ||
| 153 | </padding> | ||
| 154 | <children> | ||
| 155 | <HBox alignment="BASELINE_LEFT"> | ||
| 156 | <children> | ||
| 157 | <Label styleClass="gm-text-lg, gm-font-bold, gm-text-yes" text="YES" /> | ||
| 158 | <Region HBox.hgrow="ALWAYS" /> | ||
| 159 | <Label fx:id="lmsrYesPriceLabel" styleClass="gm-text-2xl, gm-font-semibold, gm-font-mono, gm-text-yes" /> | ||
| 160 | </children> | ||
| 161 | </HBox> | ||
| 162 | <ProgressBar fx:id="lmsrYesProgressBar" maxHeight="6.0" maxWidth="1.7976931348623157E308" minHeight="6.0" prefHeight="6.0" styleClass="gm-progress, gm-progress-yes" /> | ||
| 163 | <Label fx:id="lmsrYesSharesLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 164 | </children> | ||
| 165 | </VBox> | ||
| 166 | <VBox spacing="9.0" styleClass="gm-card-soft-no" HBox.hgrow="ALWAYS"> | ||
| 167 | <padding> | ||
| 168 | <Insets bottom="12.0" left="13.0" right="13.0" top="12.0" /> | ||
| 169 | </padding> | ||
| 170 | <children> | ||
| 171 | <HBox alignment="BASELINE_LEFT"> | ||
| 172 | <children> | ||
| 173 | <Label styleClass="gm-text-lg, gm-font-bold, gm-text-no" text="NO" /> | ||
| 174 | <Region HBox.hgrow="ALWAYS" /> | ||
| 175 | <Label fx:id="lmsrNoPriceLabel" styleClass="gm-text-2xl, gm-font-semibold, gm-font-mono, gm-text-no" /> | ||
| 176 | </children> | ||
| 177 | </HBox> | ||
| 178 | <ProgressBar fx:id="lmsrNoProgressBar" maxHeight="6.0" maxWidth="1.7976931348623157E308" minHeight="6.0" prefHeight="6.0" styleClass="gm-progress, gm-progress-no" /> | ||
| 179 | <Label fx:id="lmsrNoSharesLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 180 | </children> | ||
| 181 | </VBox> | ||
| 182 | </children> | ||
| 183 | </HBox> | ||
| 184 | <!-- LMSR Chart Block --> | ||
| 185 | <VBox spacing="8.0"> | ||
| 186 | <children> | ||
| 187 | <HBox alignment="CENTER_LEFT"> | ||
| 188 | <children> | ||
| 189 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Share price over time" /> | ||
| 190 | <Region HBox.hgrow="ALWAYS" /> | ||
| 191 | <Label fx:id="lmsrChartSubLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 192 | </children> | ||
| 193 | </HBox> | ||
| 194 | <VBox styleClass="gm-card-panel2"> | ||
| 195 | <padding> | ||
| 196 | <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> | ||
| 197 | </padding> | ||
| 198 | <children> | ||
| 199 | <LineChart fx:id="lmsrChart" animated="false" createSymbols="false" horizontalGridLinesVisible="true" legendVisible="false" prefHeight="170.0" styleClass="gm-chart, gm-chart-dual" verticalGridLinesVisible="false"> | ||
| 200 | <xAxis> | ||
| 201 | <NumberAxis autoRanging="false" forceZeroInRange="false" minorTickVisible="false" styleClass="gm-chart-axis" tickLabelsVisible="true" tickMarkVisible="true" /> | ||
| 202 | </xAxis> | ||
| 203 | <yAxis> | ||
| 204 | <NumberAxis autoRanging="false" lowerBound="0.0" minorTickVisible="false" styleClass="gm-chart-axis" tickUnit="0.25" upperBound="1.0" /> | ||
| 205 | </yAxis> | ||
| 206 | </LineChart> | ||
| 207 | </children> | ||
| 208 | </VBox> | ||
| 209 | </children> | ||
| 210 | </VBox> | ||
| 211 | </children> | ||
| 212 | </VBox> | ||
| 213 | <!-- Order Book Section --> | ||
| 214 | <VBox fx:id="orderBookSection" spacing="14.0"> | ||
| 215 | <children> | ||
| 216 | <HBox spacing="12.0"> | ||
| 217 | <children> | ||
| 218 | <!-- YES Order Book --> | ||
| 219 | <VBox styleClass="gm-card-panel2" HBox.hgrow="ALWAYS"> | ||
| 220 | <children> | ||
| 221 | <HBox alignment="CENTER_LEFT" styleClass="gm-card-soft-yes"> | ||
| 222 | <padding> | ||
| 223 | <Insets bottom="10.0" left="12.0" right="12.0" top="10.0" /> | ||
| 224 | </padding> | ||
| 225 | <children> | ||
| 226 | <Label styleClass="gm-text-base, gm-font-bold, gm-text-yes" text="YES Order Book" /> | ||
| 227 | <Region HBox.hgrow="ALWAYS" /> | ||
| 228 | <Label fx:id="yesBookCountLabel" styleClass="gm-text-xs, gm-font-regular, gm-text-ink2" /> | ||
| 229 | </children> | ||
| 230 | </HBox> | ||
| 231 | <FlowPane hgap="12.0" vgap="4.0" styleClass="gm-divider-bottom"> | ||
| 232 | <padding> | ||
| 233 | <Insets bottom="8.0" left="12.0" right="12.0" top="8.0" /> | ||
| 234 | </padding> | ||
| 235 | <children> | ||
| 236 | <HBox spacing="4.0"> | ||
| 237 | <children> | ||
| 238 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Last" /> | ||
| 239 | <Label fx:id="yesLastLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 240 | </children> | ||
| 241 | </HBox> | ||
| 242 | <HBox spacing="4.0"> | ||
| 243 | <children> | ||
| 244 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Best bid" /> | ||
| 245 | <Label fx:id="yesBestBidLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 246 | </children> | ||
| 247 | </HBox> | ||
| 248 | <HBox spacing="4.0"> | ||
| 249 | <children> | ||
| 250 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Best ask" /> | ||
| 251 | <Label fx:id="yesBestAskLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 252 | </children> | ||
| 253 | </HBox> | ||
| 254 | <HBox spacing="4.0"> | ||
| 255 | <children> | ||
| 256 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Mid" /> | ||
| 257 | <Label fx:id="yesMidLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 258 | </children> | ||
| 259 | </HBox> | ||
| 260 | <HBox spacing="4.0"> | ||
| 261 | <children> | ||
| 262 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Spread" /> | ||
| 263 | <Label fx:id="yesSpreadLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 264 | </children> | ||
| 265 | </HBox> | ||
| 266 | </children> | ||
| 267 | </FlowPane> | ||
| 268 | <HBox spacing="8.0" styleClass="gm-bg-panel3"> | ||
| 269 | <padding> | ||
| 270 | <Insets bottom="6.0" left="12.0" right="12.0" top="6.0" /> | ||
| 271 | </padding> | ||
| 272 | <children> | ||
| 273 | <Label alignment="CENTER_RIGHT" minWidth="54.0" prefWidth="54.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Price" /> | ||
| 274 | <Label alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Side" /> | ||
| 275 | <Label alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Qty" /> | ||
| 276 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="User" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 277 | </children> | ||
| 278 | </HBox> | ||
| 279 | <VBox fx:id="yesBookLadderList" /> | ||
| 280 | </children> | ||
| 281 | </VBox> | ||
| 282 | <!-- NO Order Book --> | ||
| 283 | <VBox styleClass="gm-card-panel2" HBox.hgrow="ALWAYS"> | ||
| 284 | <children> | ||
| 285 | <HBox alignment="CENTER_LEFT" styleClass="gm-card-soft-no"> | ||
| 286 | <padding> | ||
| 287 | <Insets bottom="10.0" left="12.0" right="12.0" top="10.0" /> | ||
| 288 | </padding> | ||
| 289 | <children> | ||
| 290 | <Label styleClass="gm-text-base, gm-font-bold, gm-text-no" text="NO Order Book" /> | ||
| 291 | <Region HBox.hgrow="ALWAYS" /> | ||
| 292 | <Label fx:id="noBookCountLabel" styleClass="gm-text-xs, gm-font-regular, gm-text-ink2" /> | ||
| 293 | </children> | ||
| 294 | </HBox> | ||
| 295 | <FlowPane hgap="12.0" vgap="4.0" styleClass="gm-divider-bottom"> | ||
| 296 | <padding> | ||
| 297 | <Insets bottom="8.0" left="12.0" right="12.0" top="8.0" /> | ||
| 298 | </padding> | ||
| 299 | <children> | ||
| 300 | <HBox spacing="4.0"> | ||
| 301 | <children> | ||
| 302 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Last" /> | ||
| 303 | <Label fx:id="noLastLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 304 | </children> | ||
| 305 | </HBox> | ||
| 306 | <HBox spacing="4.0"> | ||
| 307 | <children> | ||
| 308 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Best bid" /> | ||
| 309 | <Label fx:id="noBestBidLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 310 | </children> | ||
| 311 | </HBox> | ||
| 312 | <HBox spacing="4.0"> | ||
| 313 | <children> | ||
| 314 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Best ask" /> | ||
| 315 | <Label fx:id="noBestAskLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 316 | </children> | ||
| 317 | </HBox> | ||
| 318 | <HBox spacing="4.0"> | ||
| 319 | <children> | ||
| 320 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Mid" /> | ||
| 321 | <Label fx:id="noMidLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 322 | </children> | ||
| 323 | </HBox> | ||
| 324 | <HBox spacing="4.0"> | ||
| 325 | <children> | ||
| 326 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="Spread" /> | ||
| 327 | <Label fx:id="noSpreadLabel" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink" /> | ||
| 328 | </children> | ||
| 329 | </HBox> | ||
| 330 | </children> | ||
| 331 | </FlowPane> | ||
| 332 | <HBox spacing="8.0" styleClass="gm-bg-panel3"> | ||
| 333 | <padding> | ||
| 334 | <Insets bottom="6.0" left="12.0" right="12.0" top="6.0" /> | ||
| 335 | </padding> | ||
| 336 | <children> | ||
| 337 | <Label alignment="CENTER_RIGHT" minWidth="54.0" prefWidth="54.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Price" /> | ||
| 338 | <Label alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Side" /> | ||
| 339 | <Label alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Qty" /> | ||
| 340 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="User" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 341 | </children> | ||
| 342 | </HBox> | ||
| 343 | <VBox fx:id="noBookLadderList" /> | ||
| 344 | </children> | ||
| 345 | </VBox> | ||
| 346 | </children> | ||
| 347 | </HBox> | ||
| 348 | <VBox spacing="8.0"> | ||
| 349 | <children> | ||
| 350 | <HBox alignment="CENTER_LEFT"> | ||
| 351 | <children> | ||
| 352 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Last trade price over time" /> | ||
| 353 | <Region HBox.hgrow="ALWAYS" /> | ||
| 354 | <Label fx:id="obChartSubLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 355 | </children> | ||
| 356 | </HBox> | ||
| 357 | <VBox styleClass="gm-card-panel2"> | ||
| 358 | <padding> | ||
| 359 | <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> | ||
| 360 | </padding> | ||
| 361 | <children> | ||
| 362 | <LineChart fx:id="obChart" animated="false" createSymbols="false" horizontalGridLinesVisible="true" legendVisible="false" prefHeight="160.0" styleClass="gm-chart, gm-chart-single" verticalGridLinesVisible="false"> | ||
| 363 | <xAxis> | ||
| 364 | <NumberAxis autoRanging="false" forceZeroInRange="false" minorTickVisible="false" styleClass="gm-chart-axis" tickLabelsVisible="true" tickMarkVisible="true" /> | ||
| 365 | </xAxis> | ||
| 366 | <yAxis> | ||
| 367 | <NumberAxis autoRanging="false" lowerBound="0.0" minorTickVisible="false" styleClass="gm-chart-axis" tickUnit="0.25" upperBound="1.0" /> | ||
| 368 | </yAxis> | ||
| 369 | </LineChart> | ||
| 370 | </children> | ||
| 371 | </VBox> | ||
| 372 | </children> | ||
| 373 | </VBox> | ||
| 374 | </children> | ||
| 375 | </VBox> | ||
| 376 | <!-- Trade History Block --> | ||
| 377 | <VBox spacing="7.0"> | ||
| 378 | <children> | ||
| 379 | <HBox alignment="CENTER_LEFT" spacing="6.0"> | ||
| 380 | <children> | ||
| 381 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Trade history" /> | ||
| 382 | <Label styleClass="gm-text-base, gm-font-regular, gm-text-ink2" text="newest first" /> | ||
| 383 | </children> | ||
| 384 | </HBox> | ||
| 385 | <VBox styleClass="gm-card-panel"> | ||
| 386 | <children> | ||
| 387 | <HBox alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-bg-panel3"> | ||
| 388 | <padding> | ||
| 389 | <Insets bottom="8.0" left="11.0" right="11.0" top="8.0" /> | ||
| 390 | </padding> | ||
| 391 | <children> | ||
| 392 | <Label minWidth="66.0" prefWidth="66.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="#" /> | ||
| 393 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="User" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 394 | <Label minWidth="76.0" prefWidth="76.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Option" /> | ||
| 395 | <Label minWidth="92.0" prefWidth="92.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Shares" /> | ||
| 396 | <Label minWidth="88.0" prefWidth="88.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Paid" /> | ||
| 397 | </children> | ||
| 398 | </HBox> | ||
| 399 | <VBox fx:id="tradeHistoryRowsContainer" /> | ||
| 400 | </children> | ||
| 401 | </VBox> | ||
| 402 | </children> | ||
| 403 | </VBox> | ||
| 404 | <!-- Participants Section --> | ||
| 405 | <VBox spacing="7.0"> | ||
| 406 | <children> | ||
| 407 | <VBox styleClass="gm-card-panel"> | ||
| 408 | <children> | ||
| 409 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Participants"> | ||
| 410 | <padding> | ||
| 411 | <Insets bottom="4.0" /> | ||
| 412 | </padding> | ||
| 413 | </Label> | ||
| 414 | <HBox alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-bg-panel3"> | ||
| 415 | <padding> | ||
| 416 | <Insets bottom="8.0" left="11.0" right="11.0" top="8.0" /> | ||
| 417 | </padding> | ||
| 418 | <children> | ||
| 419 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="User" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 420 | <Label alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="YES" /> | ||
| 421 | <Label alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="NO" /> | ||
| 422 | <Label alignment="CENTER_RIGHT" minWidth="96.0" prefWidth="96.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Value" /> | ||
| 423 | <Label alignment="CENTER_RIGHT" minWidth="96.0" prefWidth="96.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Fees paid" /> | ||
| 424 | </children> | ||
| 425 | </HBox> | ||
| 426 | <VBox fx:id="participantsRowsContainer" /> | ||
| 427 | </children> | ||
| 428 | </VBox> | ||
| 429 | </children> | ||
| 430 | </VBox> | ||
| 431 | <!-- Resolved Banner Section --> | ||
| 432 | <HBox fx:id="resolvedBanner" alignment="CENTER_LEFT" spacing="12.0" styleClass="gm-banner-resolved"> | ||
| 433 | <padding> | ||
| 434 | <Insets bottom="12.0" left="14.0" right="14.0" top="12.0" /> | ||
| 435 | </padding> | ||
| 436 | <children> | ||
| 437 | <Label styleClass="gm-pill, gm-pill-resolved" text="RESOLVED" /> | ||
| 438 | <Label fx:id="resolvedBannerText" maxWidth="1.7976931348623157E308" styleClass="gm-text-md, gm-font-medium, gm-text-ink" wrapText="true" HBox.hgrow="ALWAYS" /> | ||
| 439 | </children> | ||
| 440 | </HBox> | ||
| 441 | </children> | ||
| 442 | </VBox> | ||
| 443 | </content> | ||
| 444 | </ScrollPane> | ||
| 445 | </children> | ||
| 446 | </VBox> | ||
| 447 | </children> | ||
| 448 | </HBox> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/users_tab.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/users_tab.fxml new file mode 100644 index 0000000..0a8ee3b --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/controllers/users_tab.fxml | |||
| @@ -0,0 +1,240 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.chart.LineChart?> | ||
| 5 | <?import javafx.scene.chart.NumberAxis?> | ||
| 6 | <?import javafx.scene.control.Button?> | ||
| 7 | <?import javafx.scene.control.Label?> | ||
| 8 | <?import javafx.scene.control.ListView?> | ||
| 9 | <?import javafx.scene.control.ScrollPane?> | ||
| 10 | <?import javafx.scene.control.TextField?> | ||
| 11 | <?import javafx.scene.layout.FlowPane?> | ||
| 12 | <?import javafx.scene.layout.HBox?> | ||
| 13 | <?import javafx.scene.layout.Priority?> | ||
| 14 | <?import javafx.scene.layout.Region?> | ||
| 15 | <?import javafx.scene.layout.StackPane?> | ||
| 16 | <?import javafx.scene.layout.VBox?> | ||
| 17 | |||
| 18 | <HBox spacing="12.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="market.guess.ui.desktop.controllers.UsersTabController"> | ||
| 19 | <padding> | ||
| 20 | <Insets bottom="12.0" left="16.0" right="16.0" top="12.0" /> | ||
| 21 | </padding> | ||
| 22 | <children> | ||
| 23 | <!-- Left Card: Users List --> | ||
| 24 | <VBox maxWidth="274.0" minWidth="274.0" prefWidth="274.0" spacing="6.0" styleClass="gm-card"> | ||
| 25 | <padding> | ||
| 26 | <Insets bottom="12.0" left="12.0" right="12.0" top="12.0" /> | ||
| 27 | </padding> | ||
| 28 | <children> | ||
| 29 | <!-- Header --> | ||
| 30 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 31 | <children> | ||
| 32 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="User" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 33 | <Label alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Balance" /> | ||
| 34 | </children> | ||
| 35 | </HBox> | ||
| 36 | <!-- Virtualized Users List --> | ||
| 37 | <ListView fx:id="userListContainer" styleClass="gm-scroll-transparent, gm-list-view" VBox.vgrow="ALWAYS" /> | ||
| 38 | <!-- Create Event Button --> | ||
| 39 | <Button maxWidth="1.7976931348623157E308" onAction="#handleCreateNewEvent" styleClass="gm-btn, gm-btn-primary" text="+ Create New Event" /> | ||
| 40 | </children> | ||
| 41 | </VBox> | ||
| 42 | <!-- Right Card: User Details --> | ||
| 43 | <VBox fx:id="userDetailCard" styleClass="gm-card" HBox.hgrow="ALWAYS"> | ||
| 44 | <children> | ||
| 45 | <!-- Header Section --> | ||
| 46 | <HBox alignment="CENTER_LEFT" spacing="14.0" styleClass="gm-divider-bottom"> | ||
| 47 | <padding> | ||
| 48 | <Insets bottom="14.0" left="16.0" right="16.0" top="14.0" /> | ||
| 49 | </padding> | ||
| 50 | <children> | ||
| 51 | <VBox spacing="5.0" HBox.hgrow="ALWAYS"> | ||
| 52 | <children> | ||
| 53 | <HBox alignment="CENTER_LEFT" spacing="9.0"> | ||
| 54 | <children> | ||
| 55 | <Label fx:id="userNameLabel" styleClass="gm-text-xl, gm-font-bold, gm-text-ink" /> | ||
| 56 | <Label fx:id="userRolePill" styleClass="gm-pill" /> | ||
| 57 | </children> | ||
| 58 | </HBox> | ||
| 59 | <Label fx:id="userSubLabel" styleClass="gm-text-base, gm-font-regular, gm-text-ink2" /> | ||
| 60 | </children> | ||
| 61 | </VBox> | ||
| 62 | <HBox alignment="CENTER_RIGHT" spacing="12.0"> | ||
| 63 | <children> | ||
| 64 | <StackPane fx:id="sparklineBox" /> | ||
| 65 | <VBox alignment="CENTER_RIGHT" spacing="4.0"> | ||
| 66 | <children> | ||
| 67 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Balance" /> | ||
| 68 | <Label fx:id="userBalanceLabel" styleClass="gm-user-balance" /> | ||
| 69 | </children> | ||
| 70 | </VBox> | ||
| 71 | </children> | ||
| 72 | </HBox> | ||
| 73 | </children> | ||
| 74 | </HBox> | ||
| 75 | <!-- Body Content (Scrollable) --> | ||
| 76 | <ScrollPane fitToWidth="true" styleClass="gm-scroll-transparent" VBox.vgrow="ALWAYS"> | ||
| 77 | <content> | ||
| 78 | <VBox spacing="14.0"> | ||
| 79 | <padding> | ||
| 80 | <Insets bottom="14.0" left="16.0" right="16.0" top="14.0" /> | ||
| 81 | </padding> | ||
| 82 | <children> | ||
| 83 | <!-- Blocked Banner --> | ||
| 84 | <HBox fx:id="blockedBanner" alignment="CENTER_LEFT" spacing="11.0" styleClass="gm-banner-blocked"> | ||
| 85 | <padding> | ||
| 86 | <Insets bottom="11.0" left="13.0" right="13.0" top="11.0" /> | ||
| 87 | </padding> | ||
| 88 | <children> | ||
| 89 | <Label styleClass="gm-pill, gm-pill-blocked" text="BLOCKED" /> | ||
| 90 | <Label maxWidth="1.7976931348623157E308" styleClass="gm-text-base, gm-font-medium, gm-text-ink" text="This account reached a negative balance. All trading actions are disabled and cannot be re-enabled — accounts cannot be topped up in Ex 2." wrapText="true" HBox.hgrow="ALWAYS" /> | ||
| 91 | </children> | ||
| 92 | </HBox> | ||
| 93 | <!-- Balance Chart Block --> | ||
| 94 | <VBox spacing="8.0"> | ||
| 95 | <children> | ||
| 96 | <HBox alignment="CENTER_LEFT"> | ||
| 97 | <children> | ||
| 98 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Account balance over time" /> | ||
| 99 | <Region HBox.hgrow="ALWAYS" /> | ||
| 100 | <Label styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" text="every trade, fee and payout" /> | ||
| 101 | </children> | ||
| 102 | </HBox> | ||
| 103 | <VBox styleClass="gm-card-panel2"> | ||
| 104 | <padding> | ||
| 105 | <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> | ||
| 106 | </padding> | ||
| 107 | <children> | ||
| 108 | <LineChart fx:id="balanceChart" animated="false" createSymbols="false" horizontalGridLinesVisible="true" legendVisible="false" prefHeight="170.0" styleClass="gm-chart, gm-chart-single" verticalGridLinesVisible="false"> | ||
| 109 | <xAxis> | ||
| 110 | <NumberAxis autoRanging="false" forceZeroInRange="false" minorTickVisible="false" styleClass="gm-chart-axis" tickLabelsVisible="true" tickMarkVisible="true" /> | ||
| 111 | </xAxis> | ||
| 112 | <yAxis> | ||
| 113 | <NumberAxis autoRanging="true" minorTickVisible="false" styleClass="gm-chart-axis" /> | ||
| 114 | </yAxis> | ||
| 115 | </LineChart> | ||
| 116 | </children> | ||
| 117 | </VBox> | ||
| 118 | </children> | ||
| 119 | </VBox> | ||
| 120 | <!-- Participation Table Block --> | ||
| 121 | <VBox spacing="7.0"> | ||
| 122 | <children> | ||
| 123 | <Label styleClass="gm-text-base, gm-font-semibold, gm-text-ink" text="Events participation / owner" /> | ||
| 124 | <VBox styleClass="gm-card-panel"> | ||
| 125 | <children> | ||
| 126 | <HBox alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-bg-panel3"> | ||
| 127 | <padding> | ||
| 128 | <Insets bottom="8.0" left="11.0" right="11.0" top="8.0" /> | ||
| 129 | </padding> | ||
| 130 | <children> | ||
| 131 | <Label maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Event" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 132 | <Label minWidth="-Infinity" prefWidth="92.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Role" /> | ||
| 133 | <Label minWidth="-Infinity" prefWidth="78.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="Type" /> | ||
| 134 | <Label alignment="CENTER_RIGHT" minWidth="78.0" prefWidth="78.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="YES" /> | ||
| 135 | <Label alignment="CENTER_RIGHT" minWidth="78.0" prefWidth="78.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="NO" /> | ||
| 136 | <Label alignment="CENTER_RIGHT" minWidth="90.0" prefWidth="90.0" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" text="P/L" /> | ||
| 137 | </children> | ||
| 138 | </HBox> | ||
| 139 | <VBox fx:id="participationRowsContainer" /> | ||
| 140 | </children> | ||
| 141 | </VBox> | ||
| 142 | </children> | ||
| 143 | </VBox> | ||
| 144 | <!-- Trade Panel Block --> | ||
| 145 | <VBox fx:id="tradePanel" spacing="9.0" styleClass="gm-card-panel2"> | ||
| 146 | <padding> | ||
| 147 | <Insets bottom="13.0" left="14.0" right="14.0" top="13.0" /> | ||
| 148 | </padding> | ||
| 149 | <children> | ||
| 150 | <Label fx:id="noTradeEventLabel" styleClass="gm-text-base, gm-font-regular, gm-text-ink2" text="Select an event above to trade." visible="false" /> | ||
| 151 | <VBox fx:id="tradeContentBox" spacing="9.0"> | ||
| 152 | <children> | ||
| 153 | <HBox alignment="BASELINE_LEFT"> | ||
| 154 | <children> | ||
| 155 | <Label fx:id="tradeEventTitle" styleClass="gm-text-lg, gm-font-bold, gm-text-ink" /> | ||
| 156 | <Region HBox.hgrow="ALWAYS" /> | ||
| 157 | <Label fx:id="tradeEventSub" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 158 | </children> | ||
| 159 | </HBox> | ||
| 160 | <HBox spacing="10.0"> | ||
| 161 | <children> | ||
| 162 | <VBox fx:id="tradeYesOptCard" onMouseClicked="#handleSelectTradeYes" spacing="6.0" styleClass="gm-option-card, gm-option-card-yes" HBox.hgrow="ALWAYS"> | ||
| 163 | <padding> | ||
| 164 | <Insets bottom="10.0" left="12.0" right="12.0" top="10.0" /> | ||
| 165 | </padding> | ||
| 166 | <children> | ||
| 167 | <HBox alignment="CENTER_LEFT"> | ||
| 168 | <children> | ||
| 169 | <Label styleClass="gm-text-md, gm-font-bold, gm-text-yes" text="YES" /> | ||
| 170 | <Region HBox.hgrow="ALWAYS" /> | ||
| 171 | <Label fx:id="tradeYesPriceLabel" styleClass="gm-text-xl, gm-font-semibold, gm-font-mono, gm-text-yes" /> | ||
| 172 | </children> | ||
| 173 | </HBox> | ||
| 174 | <Label fx:id="tradeYesHeldLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 175 | </children> | ||
| 176 | </VBox> | ||
| 177 | <VBox fx:id="tradeNoOptCard" onMouseClicked="#handleSelectTradeNo" spacing="6.0" styleClass="gm-option-card, gm-option-card-no" HBox.hgrow="ALWAYS"> | ||
| 178 | <padding> | ||
| 179 | <Insets bottom="10.0" left="12.0" right="12.0" top="10.0" /> | ||
| 180 | </padding> | ||
| 181 | <children> | ||
| 182 | <HBox alignment="CENTER_LEFT"> | ||
| 183 | <children> | ||
| 184 | <Label styleClass="gm-text-md, gm-font-bold, gm-text-no" text="NO" /> | ||
| 185 | <Region HBox.hgrow="ALWAYS" /> | ||
| 186 | <Label fx:id="tradeNoPriceLabel" styleClass="gm-text-xl, gm-font-semibold, gm-font-mono, gm-text-no" /> | ||
| 187 | </children> | ||
| 188 | </HBox> | ||
| 189 | <Label fx:id="tradeNoHeldLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 190 | </children> | ||
| 191 | </VBox> | ||
| 192 | </children> | ||
| 193 | </HBox> | ||
| 194 | <FlowPane alignment="BOTTOM_LEFT" hgap="10.0" vgap="10.0"> | ||
| 195 | <children> | ||
| 196 | <VBox spacing="5.0"> | ||
| 197 | <children> | ||
| 198 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Action" /> | ||
| 199 | <HBox> | ||
| 200 | <children> | ||
| 201 | <Button fx:id="tradeBuyBtn" onAction="#handleTradeBuy" styleClass="gm-seg, gm-seg-left, gm-seg-pad-wide" text="Buy" /> | ||
| 202 | <Button fx:id="tradeSellBtn" onAction="#handleTradeSell" styleClass="gm-seg, gm-seg-right, gm-seg-pad-wide" text="Sell" /> | ||
| 203 | </children> | ||
| 204 | </HBox> | ||
| 205 | </children> | ||
| 206 | </VBox> | ||
| 207 | <VBox spacing="5.0"> | ||
| 208 | <children> | ||
| 209 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Shares" /> | ||
| 210 | <TextField fx:id="tradeQtyField" prefWidth="96.0" styleClass="gm-textfield, gm-textfield-mono" /> | ||
| 211 | </children> | ||
| 212 | </VBox> | ||
| 213 | <VBox spacing="5.0"> | ||
| 214 | <children> | ||
| 215 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Price / share" /> | ||
| 216 | <TextField fx:id="tradePriceField" prefWidth="96.0" styleClass="gm-textfield, gm-textfield-mono" /> | ||
| 217 | </children> | ||
| 218 | </VBox> | ||
| 219 | <VBox spacing="5.0"> | ||
| 220 | <children> | ||
| 221 | <Label styleClass="gm-text-base, gm-font-medium, gm-text-ink2" text="Estimated cost" /> | ||
| 222 | <Label fx:id="tradeCostLabel" styleClass="gm-text-lg, gm-font-semibold, gm-font-mono, gm-text-ink" /> | ||
| 223 | </children> | ||
| 224 | </VBox> | ||
| 225 | <Button fx:id="tradeSubmitBtn" onAction="#handleTradeSubmit" styleClass="gm-btn, gm-btn-primary" /> | ||
| 226 | </children> | ||
| 227 | </FlowPane> | ||
| 228 | <Label fx:id="tradeHintLabel" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" wrapText="true" /> | ||
| 229 | </children> | ||
| 230 | </VBox> | ||
| 231 | </children> | ||
| 232 | </VBox> | ||
| 233 | </children> | ||
| 234 | </VBox> | ||
| 235 | </content> | ||
| 236 | </ScrollPane> | ||
| 237 | </children> | ||
| 238 | </VBox> | ||
| 239 | </children> | ||
| 240 | </HBox> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/event_list_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/event_list_item.fxml new file mode 100644 index 0000000..9d82c4a --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/event_list_item.fxml | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Region?> | ||
| 7 | <?import javafx.scene.layout.VBox?> | ||
| 8 | |||
| 9 | <fx:root type="javafx.scene.layout.VBox" spacing="5.0" styleClass="gm-list-row" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 10 | <padding> | ||
| 11 | <Insets bottom="9.0" left="11.0" right="11.0" top="9.0" /> | ||
| 12 | </padding> | ||
| 13 | <children> | ||
| 14 | <HBox alignment="CENTER_LEFT" spacing="6.0"> | ||
| 15 | <children> | ||
| 16 | <Label fx:id="numLabel" styleClass="gm-text-xs, gm-font-semibold, gm-text-ink2, gm-font-mono" /> | ||
| 17 | <Label fx:id="titleLabel" maxWidth="1.7976931348623157E308" styleClass="gm-text-md, gm-font-semibold, gm-text-ink" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 18 | </children> | ||
| 19 | </HBox> | ||
| 20 | <HBox alignment="CENTER_LEFT" spacing="8.0"> | ||
| 21 | <children> | ||
| 22 | <Label fx:id="typeLabel" styleClass="gm-text-xs, gm-font-medium, gm-text-ink2" /> | ||
| 23 | <Label fx:id="statusLabel" styleClass="gm-pill" /> | ||
| 24 | <Label fx:id="feeLabel" minWidth="0.0" styleClass="gm-text-xs, gm-font-medium, gm-text-ink2" textOverrun="ELLIPSIS" /> | ||
| 25 | <Region HBox.hgrow="ALWAYS" /> | ||
| 26 | <Label fx:id="contractLabel" alignment="CENTER_RIGHT" styleClass="gm-text-xs, gm-font-semibold, gm-font-mono, gm-text-accent" /> | ||
| 27 | </children> | ||
| 28 | </HBox> | ||
| 29 | </children> | ||
| 30 | </fx:root> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Bold.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Bold.ttf new file mode 100644 index 0000000..be823bf --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Bold.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Regular.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Regular.ttf new file mode 100644 index 0000000..013a15b --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/CaskaydiaCoveNerdFont-Regular.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Bold.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Bold.ttf new file mode 100644 index 0000000..ed37330 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Bold.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Regular.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Regular.ttf new file mode 100644 index 0000000..3fd04ba --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/FiraCodeNerdFont-Regular.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Bold.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Bold.ttf new file mode 100644 index 0000000..de2d3b3 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Bold.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Regular.ttf b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Regular.ttf new file mode 100644 index 0000000..235a07a --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/fonts/JetBrainsMonoNerdFont-Regular.ttf | |||
| Binary files differ | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/ladder_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/ladder_item.fxml new file mode 100644 index 0000000..7516d5b --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/ladder_item.fxml | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Priority?> | ||
| 7 | |||
| 8 | <fx:root type="javafx.scene.layout.HBox" alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-ladder-row" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 9 | <padding> | ||
| 10 | <Insets bottom="3.0" left="12.0" right="12.0" top="3.0" /> | ||
| 11 | </padding> | ||
| 12 | <children> | ||
| 13 | <Label fx:id="priceLabel" alignment="CENTER_RIGHT" minWidth="54.0" prefWidth="54.0" styleClass="gm-text-xs, gm-font-mono, gm-text-ink2" /> | ||
| 14 | <Label fx:id="sideLabel" alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-mono, gm-text-ink2" /> | ||
| 15 | <Label fx:id="qtyLabel" alignment="CENTER_RIGHT" minWidth="50.0" prefWidth="50.0" styleClass="gm-text-xs, gm-font-mono, gm-text-ink2" /> | ||
| 16 | <Label fx:id="userLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-xs, gm-font-medium, gm-text-ink2" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 17 | </children> | ||
| 18 | </fx:root> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/participant_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/participant_item.fxml new file mode 100644 index 0000000..840bacb --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/participant_item.fxml | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Priority?> | ||
| 7 | |||
| 8 | <fx:root type="javafx.scene.layout.HBox" alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-divider-top" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 9 | <padding> | ||
| 10 | <Insets bottom="7.0" left="11.0" right="11.0" top="7.0" /> | ||
| 11 | </padding> | ||
| 12 | <children> | ||
| 13 | <Label fx:id="userLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-sm, gm-font-medium, gm-text-ink" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 14 | <Label fx:id="yesLabel" alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-text-sm, gm-font-mono, gm-text-ink2" /> | ||
| 15 | <Label fx:id="noLabel" alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-text-sm, gm-font-mono, gm-text-ink2" /> | ||
| 16 | <Label fx:id="valueLabel" alignment="CENTER_RIGHT" minWidth="96.0" prefWidth="96.0" styleClass="gm-text-sm, gm-font-mono, gm-text-ink2" /> | ||
| 17 | <Label fx:id="feesLabel" alignment="CENTER_RIGHT" minWidth="96.0" prefWidth="96.0" styleClass="gm-text-sm, gm-font-mono, gm-text-ink2" /> | ||
| 18 | </children> | ||
| 19 | </fx:root> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/theme.css b/ui-desktop/src/main/resources/market/guess/ui/desktop/theme.css new file mode 100644 index 0000000..f2616a3 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/theme.css | |||
| @@ -0,0 +1,926 @@ | |||
| 1 | /* Guess Market desktop UI stylesheet. | ||
| 2 | * | ||
| 3 | * PALETTES & TYPOGRAPHY: | ||
| 4 | * Each skin class (.gm-skin-*) defines its own looked-up color variables (-gm-*) | ||
| 5 | * and root font-family. AppView puts exactly one skin class on the scene root, | ||
| 6 | * allowing all child rules to automatically cascade and re-resolve without | ||
| 7 | * requiring any inline CSS or manual style-string construction in Java. | ||
| 8 | * | ||
| 9 | * BUNDLED NERD FONTS: | ||
| 10 | * - Rosé Pine Dawn -> CaskaydiaCove Nerd Font (CaskaydiaCove NF) | ||
| 11 | * - Catppuccin -> JetBrainsMono Nerd Font (JetBrainsMono NF) | ||
| 12 | * - Gruvbox -> FiraCode Nerd Font (FiraCode Nerd Font) | ||
| 13 | */ | ||
| 14 | |||
| 15 | @font-face { | ||
| 16 | font-family: 'CaskaydiaCove NF'; | ||
| 17 | src: url('fonts/CaskaydiaCoveNerdFont-Regular.ttf'); | ||
| 18 | } | ||
| 19 | @font-face { | ||
| 20 | font-family: 'CaskaydiaCove NF'; | ||
| 21 | font-weight: bold; | ||
| 22 | src: url('fonts/CaskaydiaCoveNerdFont-Bold.ttf'); | ||
| 23 | } | ||
| 24 | |||
| 25 | @font-face { | ||
| 26 | font-family: 'JetBrainsMono NF'; | ||
| 27 | src: url('fonts/JetBrainsMonoNerdFont-Regular.ttf'); | ||
| 28 | } | ||
| 29 | @font-face { | ||
| 30 | font-family: 'JetBrainsMono NF'; | ||
| 31 | font-weight: bold; | ||
| 32 | src: url('fonts/JetBrainsMonoNerdFont-Bold.ttf'); | ||
| 33 | } | ||
| 34 | |||
| 35 | @font-face { | ||
| 36 | font-family: 'FiraCode Nerd Font'; | ||
| 37 | src: url('fonts/FiraCodeNerdFont-Regular.ttf'); | ||
| 38 | } | ||
| 39 | @font-face { | ||
| 40 | font-family: 'FiraCode Nerd Font'; | ||
| 41 | font-weight: bold; | ||
| 42 | src: url('fonts/FiraCodeNerdFont-Bold.ttf'); | ||
| 43 | } | ||
| 44 | |||
| 45 | /* rose-pine.dev/palette/#dawn — base/surface/overlay/highlight, iris accent, pine/love for yes/no */ | ||
| 46 | .gm-root, | ||
| 47 | .gm-root:skin-rose-pine-dawn, | ||
| 48 | .gm-skin-rose-pine-dawn { | ||
| 49 | -fx-font-family: "CaskaydiaCove NF", "CaskaydiaCove Nerd Font", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | ||
| 50 | -fx-font-size: 13px; | ||
| 51 | -gm-bg: #faf4ed; | ||
| 52 | -gm-panel: #fffaf3; | ||
| 53 | -gm-panel2: #f2e9e1; | ||
| 54 | -gm-panel3: #dfdad9; | ||
| 55 | -gm-line: #cecacd; | ||
| 56 | -gm-ink: #575279; | ||
| 57 | -gm-ink2: #797593; | ||
| 58 | -gm-accent: #907aa9; | ||
| 59 | -gm-accent-fg: #fffaf3; | ||
| 60 | -gm-btn: #f2e9e1; | ||
| 61 | -gm-btn-line: #cecacd; | ||
| 62 | -gm-yes: #286983; | ||
| 63 | -gm-no: #b4637a; | ||
| 64 | -gm-yes-soft: #e3edf0; | ||
| 65 | -gm-no-soft: #f5e6ea; | ||
| 66 | } | ||
| 67 | |||
| 68 | /* catppuccin.com Mocha — mantle/base/surface, mauve accent, green/red for yes/no */ | ||
| 69 | .gm-root:skin-catppuccin, | ||
| 70 | .gm-skin-catppuccin { | ||
| 71 | -fx-font-family: "JetBrainsMono NF", "JetBrainsMono Nerd Font", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | ||
| 72 | -fx-font-size: 13px; | ||
| 73 | -gm-bg: #181825; | ||
| 74 | -gm-panel: #1e1e2e; | ||
| 75 | -gm-panel2: #313244; | ||
| 76 | -gm-panel3: #45475a; | ||
| 77 | -gm-line: #585b70; | ||
| 78 | -gm-ink: #cdd6f4; | ||
| 79 | -gm-ink2: #a6adc8; | ||
| 80 | -gm-accent: #cba6f7; | ||
| 81 | -gm-accent-fg: #1e1e2e; | ||
| 82 | -gm-btn: #313244; | ||
| 83 | -gm-btn-line: #585b70; | ||
| 84 | -gm-yes: #a6e3a1; | ||
| 85 | -gm-no: #f38ba8; | ||
| 86 | -gm-yes-soft: #23312a; | ||
| 87 | -gm-no-soft: #34232c; | ||
| 88 | } | ||
| 89 | |||
| 90 | /* gruvbox dark (hard) — bg0_h/bg0/bg1/bg2, bright orange accent, bright green/red */ | ||
| 91 | .gm-root:skin-gruvbox, | ||
| 92 | .gm-skin-gruvbox { | ||
| 93 | -fx-font-family: "FiraCode Nerd Font", "FiraCode NF", Consolas, "Liberation Mono", Menlo, monospace; | ||
| 94 | -fx-font-size: 13.5px; | ||
| 95 | -gm-bg: #1d2021; | ||
| 96 | -gm-panel: #282828; | ||
| 97 | -gm-panel2: #3c3836; | ||
| 98 | -gm-panel3: #504945; | ||
| 99 | -gm-line: #665c54; | ||
| 100 | -gm-ink: #ebdbb2; | ||
| 101 | -gm-ink2: #a89984; | ||
| 102 | -gm-accent: #fe8019; | ||
| 103 | -gm-accent-fg: #1d2021; | ||
| 104 | -gm-btn: #3c3836; | ||
| 105 | -gm-btn-line: #665c54; | ||
| 106 | -gm-yes: #b8bb26; | ||
| 107 | -gm-no: #fb4934; | ||
| 108 | -gm-yes-soft: #2b3021; | ||
| 109 | -gm-no-soft: #3b2320; | ||
| 110 | } | ||
| 111 | |||
| 112 | /* ---- typography & text fills ------------------------------------------ */ | ||
| 113 | |||
| 114 | .root { | ||
| 115 | -fx-text-fill: -gm-ink; | ||
| 116 | } | ||
| 117 | |||
| 118 | .gm-text-ink { -fx-text-fill: -gm-ink; } | ||
| 119 | .gm-text-ink2 { -fx-text-fill: -gm-ink2; } | ||
| 120 | .gm-text-accent { -fx-text-fill: -gm-accent; } | ||
| 121 | .gm-text-accent-fg { -fx-text-fill: -gm-accent-fg; } | ||
| 122 | .gm-text-yes { -fx-text-fill: -gm-yes; } | ||
| 123 | .gm-text-no { -fx-text-fill: -gm-no; } | ||
| 124 | |||
| 125 | .gm-font-mono, | ||
| 126 | .gm-textfield-mono, | ||
| 127 | .gm-pill, | ||
| 128 | .gm-user-balance, | ||
| 129 | .gm-balance-cell, | ||
| 130 | .gm-pnl-cell, | ||
| 131 | .gm-chart-axis, | ||
| 132 | .gm-anim-tick { | ||
| 133 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 134 | } | ||
| 135 | |||
| 136 | .gm-root:skin-rose-pine-dawn .gm-font-mono, | ||
| 137 | .gm-skin-rose-pine-dawn .gm-font-mono, | ||
| 138 | .gm-root:skin-rose-pine-dawn .gm-textfield-mono, | ||
| 139 | .gm-skin-rose-pine-dawn .gm-textfield-mono, | ||
| 140 | .gm-root:skin-rose-pine-dawn .gm-pill, | ||
| 141 | .gm-skin-rose-pine-dawn .gm-pill, | ||
| 142 | .gm-root:skin-rose-pine-dawn .gm-user-balance, | ||
| 143 | .gm-skin-rose-pine-dawn .gm-user-balance, | ||
| 144 | .gm-root:skin-rose-pine-dawn .gm-balance-cell, | ||
| 145 | .gm-skin-rose-pine-dawn .gm-balance-cell, | ||
| 146 | .gm-root:skin-rose-pine-dawn .gm-pnl-cell, | ||
| 147 | .gm-skin-rose-pine-dawn .gm-pnl-cell, | ||
| 148 | .gm-root:skin-rose-pine-dawn .gm-chart-axis, | ||
| 149 | .gm-skin-rose-pine-dawn .gm-chart-axis, | ||
| 150 | .gm-root:skin-rose-pine-dawn .gm-anim-tick, | ||
| 151 | .gm-skin-rose-pine-dawn .gm-anim-tick { | ||
| 152 | -fx-font-family: "CaskaydiaCove NF", "CaskaydiaCove Nerd Font", monospace; | ||
| 153 | } | ||
| 154 | |||
| 155 | .gm-root:skin-catppuccin .gm-font-mono, | ||
| 156 | .gm-skin-catppuccin .gm-font-mono, | ||
| 157 | .gm-root:skin-catppuccin .gm-textfield-mono, | ||
| 158 | .gm-skin-catppuccin .gm-textfield-mono, | ||
| 159 | .gm-root:skin-catppuccin .gm-pill, | ||
| 160 | .gm-skin-catppuccin .gm-pill, | ||
| 161 | .gm-root:skin-catppuccin .gm-user-balance, | ||
| 162 | .gm-skin-catppuccin .gm-user-balance, | ||
| 163 | .gm-root:skin-catppuccin .gm-balance-cell, | ||
| 164 | .gm-skin-catppuccin .gm-balance-cell, | ||
| 165 | .gm-root:skin-catppuccin .gm-pnl-cell, | ||
| 166 | .gm-skin-catppuccin .gm-pnl-cell, | ||
| 167 | .gm-root:skin-catppuccin .gm-chart-axis, | ||
| 168 | .gm-skin-catppuccin .gm-chart-axis, | ||
| 169 | .gm-root:skin-catppuccin .gm-anim-tick, | ||
| 170 | .gm-skin-catppuccin .gm-anim-tick { | ||
| 171 | -fx-font-family: "JetBrainsMono NF", "JetBrainsMono Nerd Font", monospace; | ||
| 172 | } | ||
| 173 | |||
| 174 | .gm-root:skin-gruvbox .gm-font-mono, | ||
| 175 | .gm-skin-gruvbox .gm-font-mono, | ||
| 176 | .gm-root:skin-gruvbox .gm-textfield-mono, | ||
| 177 | .gm-skin-gruvbox .gm-textfield-mono, | ||
| 178 | .gm-root:skin-gruvbox .gm-pill, | ||
| 179 | .gm-skin-gruvbox .gm-pill, | ||
| 180 | .gm-root:skin-gruvbox .gm-user-balance, | ||
| 181 | .gm-skin-gruvbox .gm-user-balance, | ||
| 182 | .gm-root:skin-gruvbox .gm-balance-cell, | ||
| 183 | .gm-skin-gruvbox .gm-balance-cell, | ||
| 184 | .gm-root:skin-gruvbox .gm-pnl-cell, | ||
| 185 | .gm-skin-gruvbox .gm-pnl-cell, | ||
| 186 | .gm-root:skin-gruvbox .gm-chart-axis, | ||
| 187 | .gm-skin-gruvbox .gm-chart-axis, | ||
| 188 | .gm-root:skin-gruvbox .gm-anim-tick, | ||
| 189 | .gm-skin-gruvbox .gm-anim-tick { | ||
| 190 | -fx-font-family: "FiraCode Nerd Font", "FiraCode NF", monospace; | ||
| 191 | } | ||
| 192 | .gm-font-bold { -fx-font-weight: 700; } | ||
| 193 | .gm-font-semibold { -fx-font-weight: 600; } | ||
| 194 | .gm-font-medium { -fx-font-weight: 500; } | ||
| 195 | .gm-font-regular { -fx-font-weight: 400; } | ||
| 196 | |||
| 197 | .gm-text-xs { -fx-font-size: 10.5px; } | ||
| 198 | .gm-text-sm { -fx-font-size: 11.5px; } | ||
| 199 | .gm-text-base { -fx-font-size: 12.5px; } | ||
| 200 | .gm-text-md { -fx-font-size: 13px; } | ||
| 201 | .gm-text-lg { -fx-font-size: 14px; } | ||
| 202 | .gm-text-xl { -fx-font-size: 17px; } | ||
| 203 | .gm-text-2xl { -fx-font-size: 22px; } | ||
| 204 | |||
| 205 | /* ---- window chrome & containers --------------------------------------- */ | ||
| 206 | |||
| 207 | .gm-chrome { | ||
| 208 | -fx-background-color: -gm-bg; | ||
| 209 | } | ||
| 210 | |||
| 211 | .gm-bg-panel { | ||
| 212 | -fx-background-color: -gm-panel; | ||
| 213 | } | ||
| 214 | |||
| 215 | .gm-bg-panel2 { | ||
| 216 | -fx-background-color: -gm-panel2; | ||
| 217 | } | ||
| 218 | |||
| 219 | .gm-bg-panel3 { | ||
| 220 | -fx-background-color: -gm-panel3; | ||
| 221 | } | ||
| 222 | |||
| 223 | .gm-card { | ||
| 224 | -fx-background-color: -gm-panel; | ||
| 225 | -fx-border-color: -gm-line; | ||
| 226 | -fx-border-width: 1; | ||
| 227 | -fx-background-radius: 7; | ||
| 228 | -fx-border-radius: 7; | ||
| 229 | } | ||
| 230 | |||
| 231 | .gm-card-panel2 { | ||
| 232 | -fx-background-color: -gm-panel2; | ||
| 233 | -fx-border-color: -gm-line; | ||
| 234 | -fx-border-width: 1; | ||
| 235 | -fx-background-radius: 6; | ||
| 236 | -fx-border-radius: 6; | ||
| 237 | } | ||
| 238 | |||
| 239 | .gm-card-panel { | ||
| 240 | -fx-background-color: -gm-panel; | ||
| 241 | -fx-border-color: -gm-line; | ||
| 242 | -fx-border-width: 1; | ||
| 243 | -fx-background-radius: 6; | ||
| 244 | -fx-border-radius: 6; | ||
| 245 | } | ||
| 246 | |||
| 247 | .gm-card-soft-yes { | ||
| 248 | -fx-background-color: -gm-yes-soft; | ||
| 249 | -fx-border-color: -gm-line; | ||
| 250 | -fx-border-width: 1; | ||
| 251 | -fx-background-radius: 6; | ||
| 252 | -fx-border-radius: 6; | ||
| 253 | } | ||
| 254 | |||
| 255 | .gm-card-soft-no { | ||
| 256 | -fx-background-color: -gm-no-soft; | ||
| 257 | -fx-border-color: -gm-line; | ||
| 258 | -fx-border-width: 1; | ||
| 259 | -fx-background-radius: 6; | ||
| 260 | -fx-border-radius: 6; | ||
| 261 | } | ||
| 262 | |||
| 263 | .gm-banner-blocked { | ||
| 264 | -fx-background-color: -gm-no-soft; | ||
| 265 | -fx-border-color: -gm-no; | ||
| 266 | -fx-border-width: 1; | ||
| 267 | -fx-background-radius: 6; | ||
| 268 | -fx-border-radius: 6; | ||
| 269 | } | ||
| 270 | |||
| 271 | .gm-banner-resolved { | ||
| 272 | -fx-background-color: -gm-yes-soft; | ||
| 273 | -fx-border-color: -gm-line; | ||
| 274 | -fx-border-width: 1; | ||
| 275 | -fx-background-radius: 6; | ||
| 276 | -fx-border-radius: 6; | ||
| 277 | } | ||
| 278 | |||
| 279 | .gm-divider-bottom { | ||
| 280 | -fx-border-color: transparent transparent -gm-line transparent; | ||
| 281 | -fx-border-width: 0 0 1 0; | ||
| 282 | } | ||
| 283 | |||
| 284 | .gm-divider-top { | ||
| 285 | -fx-border-color: -gm-line transparent transparent transparent; | ||
| 286 | -fx-border-width: 1 0 0 0; | ||
| 287 | } | ||
| 288 | |||
| 289 | .gm-hline { | ||
| 290 | -fx-background-color: -gm-line; | ||
| 291 | -fx-pref-height: 1; | ||
| 292 | -fx-min-height: 1; | ||
| 293 | } | ||
| 294 | |||
| 295 | /* ---- buttons & interactive controls ----------------------------------- */ | ||
| 296 | |||
| 297 | .gm-btn { | ||
| 298 | -fx-border-width: 1; | ||
| 299 | -fx-background-radius: 5; | ||
| 300 | -fx-border-radius: 5; | ||
| 301 | -fx-font-size: 13px; | ||
| 302 | -fx-font-weight: 600; | ||
| 303 | -fx-padding: 8 15; | ||
| 304 | -fx-cursor: hand; | ||
| 305 | } | ||
| 306 | |||
| 307 | .gm-btn:hover { | ||
| 308 | -fx-opacity: 0.92; | ||
| 309 | } | ||
| 310 | |||
| 311 | .gm-btn:disabled { | ||
| 312 | -fx-opacity: 0.45; | ||
| 313 | -fx-cursor: default; | ||
| 314 | } | ||
| 315 | |||
| 316 | .gm-btn-primary { | ||
| 317 | -fx-background-color: -gm-accent; | ||
| 318 | -fx-text-fill: -gm-accent-fg; | ||
| 319 | -fx-border-color: -gm-accent; | ||
| 320 | } | ||
| 321 | |||
| 322 | .gm-btn-secondary { | ||
| 323 | -fx-background-color: -gm-btn; | ||
| 324 | -fx-text-fill: -gm-ink; | ||
| 325 | -fx-border-color: -gm-btn-line; | ||
| 326 | } | ||
| 327 | |||
| 328 | .gm-btn-destructive { | ||
| 329 | -fx-background-color: -gm-no; | ||
| 330 | -fx-text-fill: -gm-accent-fg; | ||
| 331 | -fx-border-color: -gm-no; | ||
| 332 | -fx-border-width: 1; | ||
| 333 | -fx-background-radius: 5; | ||
| 334 | -fx-border-radius: 5; | ||
| 335 | -fx-font-size: 12.5px; | ||
| 336 | -fx-font-weight: 600; | ||
| 337 | -fx-padding: 9 17; | ||
| 338 | -fx-cursor: hand; | ||
| 339 | } | ||
| 340 | |||
| 341 | .gm-btn-destructive:disabled { | ||
| 342 | -fx-opacity: 0.5; | ||
| 343 | -fx-cursor: default; | ||
| 344 | } | ||
| 345 | |||
| 346 | .gm-app-scroll, | ||
| 347 | .gm-app-scroll > .viewport { | ||
| 348 | -fx-background-color: transparent; | ||
| 349 | -fx-background-insets: 0; | ||
| 350 | -fx-padding: 0; | ||
| 351 | } | ||
| 352 | |||
| 353 | .gm-tabs > .tab-header-area { | ||
| 354 | -fx-padding: 6 16 0 16; | ||
| 355 | } | ||
| 356 | |||
| 357 | .gm-tabs > .tab-header-area > .tab-header-background { | ||
| 358 | -fx-background-color: -gm-line, -gm-panel; | ||
| 359 | -fx-background-insets: 0, 0 0 1 0; | ||
| 360 | } | ||
| 361 | |||
| 362 | .gm-tabs > .tab-content-area { | ||
| 363 | -fx-background-color: transparent; | ||
| 364 | } | ||
| 365 | |||
| 366 | .gm-tabs > .tab-header-area > .headers-region > .tab { | ||
| 367 | -fx-background-color: -gm-panel2; | ||
| 368 | -fx-background-insets: 0 1 0 1; | ||
| 369 | -fx-border-color: -gm-line; | ||
| 370 | -fx-border-width: 1 1 0 1; | ||
| 371 | -fx-border-insets: 0 1 0 1; | ||
| 372 | -fx-background-radius: 6 6 0 0; | ||
| 373 | -fx-border-radius: 6 6 0 0; | ||
| 374 | -fx-padding: 9 20; | ||
| 375 | -fx-cursor: hand; | ||
| 376 | } | ||
| 377 | |||
| 378 | .gm-tabs > .tab-header-area > .headers-region > .tab:selected { | ||
| 379 | -fx-background-color: -gm-bg; | ||
| 380 | } | ||
| 381 | |||
| 382 | .gm-tabs > .tab-header-area > .headers-region > .tab .tab-label { | ||
| 383 | -fx-text-fill: -gm-ink2; | ||
| 384 | -fx-font-size: 13px; | ||
| 385 | -fx-font-weight: 600; | ||
| 386 | } | ||
| 387 | |||
| 388 | .gm-tabs > .tab-header-area > .headers-region > .tab:selected .tab-label { | ||
| 389 | -fx-text-fill: -gm-ink; | ||
| 390 | } | ||
| 391 | |||
| 392 | .gm-tabs > .tab-header-area > .headers-region > .tab:selected .focus-indicator { | ||
| 393 | -fx-border-color: transparent; | ||
| 394 | } | ||
| 395 | |||
| 396 | .gm-toggle { | ||
| 397 | -fx-background-color: -gm-btn; | ||
| 398 | -fx-text-fill: -gm-ink2; | ||
| 399 | -fx-border-color: -gm-btn-line; | ||
| 400 | -fx-border-width: 1; | ||
| 401 | -fx-background-radius: 4; | ||
| 402 | -fx-border-radius: 4; | ||
| 403 | -fx-font-size: 12px; | ||
| 404 | -fx-font-weight: 500; | ||
| 405 | -fx-padding: 5 11; | ||
| 406 | -fx-cursor: hand; | ||
| 407 | } | ||
| 408 | |||
| 409 | .gm-toggle:selected { | ||
| 410 | -fx-background-color: -gm-accent; | ||
| 411 | -fx-text-fill: -gm-accent-fg; | ||
| 412 | -fx-border-color: -gm-accent; | ||
| 413 | } | ||
| 414 | |||
| 415 | .gm-seg { | ||
| 416 | -fx-background-color: -gm-btn; | ||
| 417 | -fx-text-fill: -gm-ink; | ||
| 418 | -fx-border-color: -gm-btn-line; | ||
| 419 | -fx-border-width: 1; | ||
| 420 | -fx-font-size: 12px; | ||
| 421 | -fx-font-weight: 600; | ||
| 422 | -fx-cursor: hand; | ||
| 423 | } | ||
| 424 | |||
| 425 | .gm-seg:selected { | ||
| 426 | -fx-background-color: -gm-accent; | ||
| 427 | -fx-text-fill: -gm-accent-fg; | ||
| 428 | } | ||
| 429 | |||
| 430 | .gm-seg:disabled { | ||
| 431 | -fx-opacity: 0.45; | ||
| 432 | -fx-cursor: default; | ||
| 433 | } | ||
| 434 | |||
| 435 | .gm-seg-left { | ||
| 436 | -fx-background-radius: 5 0 0 5; | ||
| 437 | -fx-border-radius: 5 0 0 5; | ||
| 438 | } | ||
| 439 | |||
| 440 | .gm-seg-right { | ||
| 441 | -fx-background-radius: 0 5 5 0; | ||
| 442 | -fx-border-radius: 0 5 5 0; | ||
| 443 | } | ||
| 444 | |||
| 445 | .gm-seg-pad-tight { | ||
| 446 | -fx-padding: 8 0; | ||
| 447 | } | ||
| 448 | |||
| 449 | .gm-seg-pad-wide { | ||
| 450 | -fx-padding: 8 15; | ||
| 451 | } | ||
| 452 | |||
| 453 | .gm-textfield { | ||
| 454 | -fx-background-color: -gm-panel2; | ||
| 455 | -fx-border-color: -gm-btn-line; | ||
| 456 | -fx-border-width: 1; | ||
| 457 | -fx-background-radius: 5; | ||
| 458 | -fx-border-radius: 5; | ||
| 459 | -fx-font-size: 13px; | ||
| 460 | -fx-text-fill: -gm-ink; | ||
| 461 | -fx-padding: 8 10; | ||
| 462 | } | ||
| 463 | |||
| 464 | .gm-textfield:disabled { | ||
| 465 | -fx-opacity: 0.45; | ||
| 466 | } | ||
| 467 | |||
| 468 | .gm-textfield:error, | ||
| 469 | .gm-textfield.error { | ||
| 470 | -fx-border-color: -gm-no; | ||
| 471 | -fx-border-width: 1.5; | ||
| 472 | } | ||
| 473 | |||
| 474 | .gm-textfield-mono { | ||
| 475 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 476 | } | ||
| 477 | |||
| 478 | .gm-textarea { | ||
| 479 | -fx-control-inner-background: -gm-panel2; | ||
| 480 | -fx-border-color: -gm-btn-line; | ||
| 481 | -fx-border-radius: 5; | ||
| 482 | -fx-background-radius: 5; | ||
| 483 | -fx-font-size: 13px; | ||
| 484 | -fx-text-fill: -gm-ink; | ||
| 485 | } | ||
| 486 | |||
| 487 | .gm-select-base { | ||
| 488 | -fx-border-color: -gm-btn-line; | ||
| 489 | -fx-border-radius: 5; | ||
| 490 | -fx-background-radius: 5; | ||
| 491 | -fx-font-size: 12.5px; | ||
| 492 | -fx-text-base-color: -gm-ink; | ||
| 493 | } | ||
| 494 | |||
| 495 | .gm-select-btn-bg { | ||
| 496 | -fx-background-color: -gm-btn; | ||
| 497 | } | ||
| 498 | |||
| 499 | .gm-select-panel2-bg { | ||
| 500 | -fx-background-color: -gm-panel2; | ||
| 501 | } | ||
| 502 | |||
| 503 | .gm-checkbox { | ||
| 504 | -fx-text-fill: -gm-ink2; | ||
| 505 | -fx-font-size: 12.5px; | ||
| 506 | } | ||
| 507 | |||
| 508 | .gm-checkbox-strong { | ||
| 509 | -fx-text-fill: -gm-ink; | ||
| 510 | -fx-font-size: 12.5px; | ||
| 511 | } | ||
| 512 | |||
| 513 | .gm-link-button { | ||
| 514 | -fx-background-color: transparent; | ||
| 515 | -fx-text-fill: -gm-ink2; | ||
| 516 | -fx-font-size: 14.5px; | ||
| 517 | -fx-underline: false; | ||
| 518 | -fx-cursor: hand; | ||
| 519 | -fx-border-width: 0; | ||
| 520 | } | ||
| 521 | |||
| 522 | .gm-toast { | ||
| 523 | -fx-background-color: -gm-ink; | ||
| 524 | -fx-text-fill: -gm-bg; | ||
| 525 | -fx-background-radius: 6; | ||
| 526 | -fx-padding: 10 16; | ||
| 527 | -fx-font-size: 12.5px; | ||
| 528 | -fx-font-weight: 600; | ||
| 529 | } | ||
| 530 | |||
| 531 | .gm-scroll-transparent { | ||
| 532 | -fx-background-color: transparent; | ||
| 533 | -fx-background: transparent; | ||
| 534 | } | ||
| 535 | |||
| 536 | .gm-scroll-transparent > .viewport { | ||
| 537 | -fx-background-color: transparent; | ||
| 538 | } | ||
| 539 | |||
| 540 | /* ---- badges, pills, traffic lights ------------------------------------ */ | ||
| 541 | |||
| 542 | .gm-pill { | ||
| 543 | -fx-background-radius: 3; | ||
| 544 | -fx-padding: 3 6; | ||
| 545 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 546 | -fx-font-size: 10px; | ||
| 547 | -fx-font-weight: 600; | ||
| 548 | -fx-background-color: -gm-panel3; | ||
| 549 | -fx-text-fill: -gm-ink2; | ||
| 550 | } | ||
| 551 | |||
| 552 | /* Pseudo-class states for pills */ | ||
| 553 | .gm-pill:active { -fx-background-color: -gm-yes-soft; -fx-text-fill: -gm-yes; } | ||
| 554 | .gm-pill:closed { -fx-background-color: -gm-no-soft; -fx-text-fill: -gm-no; } | ||
| 555 | .gm-pill:idle { -fx-background-color: -gm-panel3; -fx-text-fill: -gm-ink2; } | ||
| 556 | .gm-pill:blocked { -fx-background-color: -gm-no; -fx-text-fill: -gm-accent-fg; } | ||
| 557 | .gm-pill:mm { -fx-background-color: -gm-accent; -fx-text-fill: -gm-accent-fg; } | ||
| 558 | .gm-pill:trader { -fx-background-color: -gm-panel3; -fx-text-fill: -gm-ink2; } | ||
| 559 | .gm-pill:resolved { -fx-background-color: -gm-yes; -fx-text-fill: -gm-accent-fg; } | ||
| 560 | |||
| 561 | .gm-pill-active { -fx-background-color: -gm-yes-soft; -fx-text-fill: -gm-yes; } | ||
| 562 | .gm-pill-closed { -fx-background-color: -gm-no-soft; -fx-text-fill: -gm-no; } | ||
| 563 | .gm-pill-idle { -fx-background-color: -gm-panel3; -fx-text-fill: -gm-ink2; } | ||
| 564 | .gm-pill-blocked { -fx-background-color: -gm-no; -fx-text-fill: -gm-accent-fg; } | ||
| 565 | .gm-pill-mm { -fx-background-color: -gm-accent; -fx-text-fill: -gm-accent-fg; } | ||
| 566 | .gm-pill-trader { -fx-background-color: -gm-panel3; -fx-text-fill: -gm-ink2; } | ||
| 567 | .gm-pill-resolved { -fx-background-color: -gm-yes; -fx-text-fill: -gm-accent-fg; } | ||
| 568 | |||
| 569 | /* ---- user balance ---------------------------------------------------- */ | ||
| 570 | .gm-user-balance { | ||
| 571 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 572 | -fx-font-size: 22px; | ||
| 573 | -fx-font-weight: 600; | ||
| 574 | -fx-text-fill: -gm-accent; | ||
| 575 | } | ||
| 576 | |||
| 577 | .gm-user-balance:negative { | ||
| 578 | -fx-text-fill: -gm-no; | ||
| 579 | } | ||
| 580 | |||
| 581 | /* ---- traffic lights with macOS on-hover symbols ---------------------- */ | ||
| 582 | |||
| 583 | .gm-traffic-lights { | ||
| 584 | -fx-alignment: center-left; | ||
| 585 | } | ||
| 586 | |||
| 587 | .gm-traffic-dot { | ||
| 588 | -fx-min-width: 12px; | ||
| 589 | -fx-min-height: 12px; | ||
| 590 | -fx-pref-width: 12px; | ||
| 591 | -fx-pref-height: 12px; | ||
| 592 | -fx-max-width: 12px; | ||
| 593 | -fx-max-height: 12px; | ||
| 594 | -fx-background-radius: 999px; | ||
| 595 | -fx-alignment: center; | ||
| 596 | -fx-cursor: hand; | ||
| 597 | } | ||
| 598 | |||
| 599 | .gm-traffic-icon { | ||
| 600 | -fx-fill: transparent; | ||
| 601 | -fx-stroke: transparent; | ||
| 602 | -fx-stroke-width: 1.1; | ||
| 603 | -fx-stroke-line-cap: round; | ||
| 604 | } | ||
| 605 | |||
| 606 | /* Reveal glyphs on traffic lights cluster hover */ | ||
| 607 | .gm-traffic-lights:hover .gm-traffic-icon { | ||
| 608 | -fx-stroke: rgba(0, 0, 0, 0.68); | ||
| 609 | } | ||
| 610 | |||
| 611 | .gm-traffic-dot:hover { -fx-opacity: 0.85; } | ||
| 612 | .gm-traffic-dot:pressed { -fx-opacity: 0.70; } | ||
| 613 | |||
| 614 | .gm-traffic-dot-no { -fx-background-color: -gm-no; } | ||
| 615 | .gm-traffic-dot-accent { -fx-background-color: -gm-accent; } | ||
| 616 | .gm-traffic-dot-yes { -fx-background-color: -gm-yes; } | ||
| 617 | |||
| 618 | /* ---- list rows, tables & option cards --------------------------------- */ | ||
| 619 | |||
| 620 | .gm-balance-cell { | ||
| 621 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 622 | -fx-font-size: 13px; | ||
| 623 | -fx-font-weight: 600; | ||
| 624 | -fx-text-fill: -gm-accent; | ||
| 625 | } | ||
| 626 | .gm-balance-cell:negative { | ||
| 627 | -fx-text-fill: -gm-no; | ||
| 628 | } | ||
| 629 | |||
| 630 | .gm-pnl-cell { | ||
| 631 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 632 | -fx-font-size: 12px; | ||
| 633 | -fx-font-weight: 600; | ||
| 634 | -fx-text-fill: -gm-yes; | ||
| 635 | } | ||
| 636 | .gm-pnl-cell:negative { | ||
| 637 | -fx-text-fill: -gm-no; | ||
| 638 | } | ||
| 639 | |||
| 640 | .gm-list-view { | ||
| 641 | -fx-background-color: transparent; | ||
| 642 | -fx-background-insets: 0; | ||
| 643 | -fx-padding: 0; | ||
| 644 | -fx-border-color: transparent; | ||
| 645 | -fx-border-width: 0; | ||
| 646 | } | ||
| 647 | |||
| 648 | .gm-list-view .list-cell { | ||
| 649 | -fx-background-color: transparent; | ||
| 650 | -fx-background-insets: 0; | ||
| 651 | -fx-padding: 1 0; | ||
| 652 | } | ||
| 653 | |||
| 654 | .gm-list-view .list-cell:empty { | ||
| 655 | -fx-background-color: transparent; | ||
| 656 | } | ||
| 657 | |||
| 658 | .gm-list-view .list-cell:filled:selected { | ||
| 659 | -fx-background-color: transparent; | ||
| 660 | } | ||
| 661 | |||
| 662 | .gm-list-view:focused .list-cell:filled:focused:selected { | ||
| 663 | -fx-background-color: transparent; | ||
| 664 | } | ||
| 665 | |||
| 666 | .gm-list-row { | ||
| 667 | -fx-background-color: transparent; | ||
| 668 | -fx-border-color: transparent; | ||
| 669 | -fx-border-width: 0 0 0 3; | ||
| 670 | -fx-background-radius: 5; | ||
| 671 | -fx-cursor: hand; | ||
| 672 | } | ||
| 673 | |||
| 674 | .gm-list-row:hover { | ||
| 675 | -fx-background-color: -gm-panel2; | ||
| 676 | } | ||
| 677 | |||
| 678 | .gm-list-row:selected { | ||
| 679 | -fx-background-color: -gm-panel3; | ||
| 680 | -fx-border-color: -gm-accent; | ||
| 681 | } | ||
| 682 | |||
| 683 | .gm-ladder-row { | ||
| 684 | -fx-border-color: -gm-line transparent transparent transparent; | ||
| 685 | -fx-border-width: 1 0 0 0; | ||
| 686 | } | ||
| 687 | |||
| 688 | .gm-ladder-row:bid, | ||
| 689 | .gm-ladder-row-bid { | ||
| 690 | -fx-background-color: -gm-yes-soft; | ||
| 691 | } | ||
| 692 | |||
| 693 | .gm-ladder-row:ask, | ||
| 694 | .gm-ladder-row-ask { | ||
| 695 | -fx-background-color: -gm-no-soft; | ||
| 696 | } | ||
| 697 | |||
| 698 | .gm-participation-row { | ||
| 699 | -fx-border-color: -gm-line transparent transparent transparent; | ||
| 700 | -fx-border-width: 1 0 0 0; | ||
| 701 | -fx-cursor: hand; | ||
| 702 | } | ||
| 703 | |||
| 704 | .gm-participation-row:hover { | ||
| 705 | -fx-background-color: -gm-panel2; | ||
| 706 | } | ||
| 707 | |||
| 708 | .gm-participation-row:selected { | ||
| 709 | -fx-background-color: -gm-panel3; | ||
| 710 | } | ||
| 711 | |||
| 712 | .gm-option-card { | ||
| 713 | -fx-background-radius: 6; | ||
| 714 | -fx-border-radius: 6; | ||
| 715 | -fx-cursor: hand; | ||
| 716 | -fx-background-color: -gm-panel; | ||
| 717 | -fx-border-color: -gm-line; | ||
| 718 | -fx-border-width: 1; | ||
| 719 | } | ||
| 720 | |||
| 721 | .gm-option-card-yes:selected { | ||
| 722 | -fx-background-color: -gm-yes-soft; | ||
| 723 | -fx-border-color: -gm-yes; | ||
| 724 | -fx-border-width: 1.5; | ||
| 725 | } | ||
| 726 | |||
| 727 | .gm-option-card-no:selected { | ||
| 728 | -fx-background-color: -gm-no-soft; | ||
| 729 | -fx-border-color: -gm-no; | ||
| 730 | -fx-border-width: 1.5; | ||
| 731 | } | ||
| 732 | |||
| 733 | /* ---- modals & dialogs ------------------------------------------------- */ | ||
| 734 | |||
| 735 | .gm-modal-scrim { | ||
| 736 | -fx-background-color: rgba(10, 10, 10, 0.42); | ||
| 737 | } | ||
| 738 | |||
| 739 | .gm-modal-card { | ||
| 740 | -fx-background-color: -gm-panel; | ||
| 741 | -fx-background: -gm-panel; | ||
| 742 | -fx-background-radius: 8; | ||
| 743 | -fx-border-radius: 8; | ||
| 744 | -fx-border-color: -gm-line; | ||
| 745 | } | ||
| 746 | |||
| 747 | .gm-resolve-option { | ||
| 748 | -fx-border-width: 1; | ||
| 749 | -fx-background-radius: 6; | ||
| 750 | -fx-border-radius: 6; | ||
| 751 | -fx-font-size: 14px; | ||
| 752 | -fx-font-weight: 700; | ||
| 753 | -fx-padding: 14 0; | ||
| 754 | -fx-cursor: hand; | ||
| 755 | -fx-background-color: -gm-panel2; | ||
| 756 | -fx-border-color: -gm-line; | ||
| 757 | } | ||
| 758 | |||
| 759 | .gm-resolve-option-yes { | ||
| 760 | -fx-text-fill: -gm-yes; | ||
| 761 | } | ||
| 762 | |||
| 763 | .gm-resolve-option-yes:selected { | ||
| 764 | -fx-background-color: -gm-yes-soft; | ||
| 765 | -fx-border-color: -gm-yes; | ||
| 766 | -fx-border-width: 1.5; | ||
| 767 | } | ||
| 768 | |||
| 769 | .gm-resolve-option-no { | ||
| 770 | -fx-text-fill: -gm-no; | ||
| 771 | } | ||
| 772 | |||
| 773 | .gm-resolve-option-no:selected { | ||
| 774 | -fx-background-color: -gm-no-soft; | ||
| 775 | -fx-border-color: -gm-no; | ||
| 776 | -fx-border-width: 1.5; | ||
| 777 | } | ||
| 778 | |||
| 779 | /* ---- progress bars ---------------------------------------------------- */ | ||
| 780 | |||
| 781 | .gm-progress { | ||
| 782 | -fx-control-inner-background: -gm-panel3; | ||
| 783 | } | ||
| 784 | |||
| 785 | .gm-progress > .track { | ||
| 786 | -fx-background-color: -gm-panel3; | ||
| 787 | -fx-background-insets: 0; | ||
| 788 | -fx-background-radius: 3px; | ||
| 789 | -fx-padding: 0; | ||
| 790 | } | ||
| 791 | |||
| 792 | .gm-progress > .bar { | ||
| 793 | -fx-background-insets: 0; | ||
| 794 | -fx-background-radius: 3px; | ||
| 795 | -fx-padding: 0; | ||
| 796 | } | ||
| 797 | |||
| 798 | .gm-progress-accent > .bar { | ||
| 799 | -fx-background-color: -gm-accent; | ||
| 800 | } | ||
| 801 | |||
| 802 | .gm-progress-yes > .bar { | ||
| 803 | -fx-background-color: -gm-yes; | ||
| 804 | } | ||
| 805 | |||
| 806 | .gm-progress-no > .bar { | ||
| 807 | -fx-background-color: -gm-no; | ||
| 808 | } | ||
| 809 | |||
| 810 | /* ---- charts ----------------------------------------------------------- */ | ||
| 811 | |||
| 812 | .gm-chart { | ||
| 813 | -fx-background-color: transparent; | ||
| 814 | } | ||
| 815 | |||
| 816 | .gm-chart-axis { | ||
| 817 | -fx-tick-label-fill: -gm-ink2; | ||
| 818 | -fx-tick-mark-stroke: -gm-ink2; | ||
| 819 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 820 | -fx-font-size: 10px; | ||
| 821 | } | ||
| 822 | |||
| 823 | .gm-chart-axis .axis-tick-mark, | ||
| 824 | .gm-chart-axis .axis-minor-tick-mark { | ||
| 825 | -fx-fill: null; | ||
| 826 | -fx-stroke: -gm-ink2; | ||
| 827 | } | ||
| 828 | |||
| 829 | .gm-chart-axis:bottom { | ||
| 830 | -fx-border-color: -gm-line transparent transparent transparent; | ||
| 831 | } | ||
| 832 | |||
| 833 | .gm-chart-axis:left { | ||
| 834 | -fx-border-color: transparent -gm-line transparent transparent; | ||
| 835 | } | ||
| 836 | |||
| 837 | .gm-chart .chart-line-symbol { | ||
| 838 | -fx-background-color: transparent, transparent; | ||
| 839 | -fx-background-radius: 0; | ||
| 840 | -fx-padding: 0; | ||
| 841 | } | ||
| 842 | |||
| 843 | .gm-chart .chart-series-line { | ||
| 844 | -fx-stroke-width: 2.5px; | ||
| 845 | } | ||
| 846 | |||
| 847 | .gm-chart-single .series0.chart-series-line { | ||
| 848 | -fx-stroke: -gm-accent; | ||
| 849 | } | ||
| 850 | |||
| 851 | .gm-chart-dual .series0.chart-series-line { | ||
| 852 | -fx-stroke: -gm-yes; | ||
| 853 | } | ||
| 854 | |||
| 855 | .gm-chart-dual .series1.chart-series-line { | ||
| 856 | -fx-stroke: -gm-no; | ||
| 857 | } | ||
| 858 | |||
| 859 | |||
| 860 | /* ---- decorative animations (see Anim.java) ---------------------------- */ | ||
| 861 | |||
| 862 | .gm-anim-rule { -fx-stroke: -gm-line; } | ||
| 863 | |||
| 864 | .gm-anim-tick { | ||
| 865 | -fx-fill: -gm-ink2; | ||
| 866 | -fx-font-family: "CaskaydiaCove NF", "JetBrainsMono NF", "FiraCode Nerd Font", monospace; | ||
| 867 | -fx-font-size: 9px; | ||
| 868 | } | ||
| 869 | |||
| 870 | .gm-anim-curve-yes { -fx-stroke: -gm-yes; } | ||
| 871 | .gm-anim-curve-no { -fx-stroke: -gm-no; } | ||
| 872 | .gm-anim-dot-yes { -fx-fill: -gm-yes; } | ||
| 873 | .gm-anim-dot-no { -fx-fill: -gm-no; } | ||
| 874 | |||
| 875 | /* LIVE pill: bars and label take the pill's status colour */ | ||
| 876 | .gm-live-pill { -fx-padding: 3 7; } | ||
| 877 | |||
| 878 | .gm-anim-bar { -fx-fill: -gm-ink2; } | ||
| 879 | .gm-live-label { -fx-text-fill: -gm-ink2; } | ||
| 880 | |||
| 881 | .gm-pill:active .gm-anim-bar { -fx-fill: -gm-yes; } | ||
| 882 | .gm-pill:active .gm-live-label { -fx-text-fill: -gm-yes; } | ||
| 883 | .gm-pill:closed .gm-anim-bar { -fx-fill: -gm-no; } | ||
| 884 | .gm-pill:closed .gm-live-label { -fx-text-fill: -gm-no; } | ||
| 885 | |||
| 886 | /* Balance sparkline: stroke follows the sign of the balance */ | ||
| 887 | .gm-anim-spark { -fx-stroke: -gm-accent; } | ||
| 888 | .gm-anim-spark-dot { -fx-fill: -gm-accent; } | ||
| 889 | |||
| 890 | .gm-anim-sparkline:negative .gm-anim-spark { -fx-stroke: -gm-no; } | ||
| 891 | .gm-anim-sparkline:negative .gm-anim-spark-dot { -fx-fill: -gm-no; } | ||
| 892 | |||
| 893 | /* Animation toggle button & spinner */ | ||
| 894 | .gm-btn-icon { | ||
| 895 | -fx-padding: 4 8; | ||
| 896 | -fx-min-width: 34px; | ||
| 897 | -fx-min-height: 30px; | ||
| 898 | -fx-pref-height: 30px; | ||
| 899 | -fx-alignment: center; | ||
| 900 | } | ||
| 901 | |||
| 902 | .gm-btn-icon:selected { | ||
| 903 | -fx-background-color: -gm-panel3; | ||
| 904 | -fx-border-color: -gm-accent; | ||
| 905 | } | ||
| 906 | |||
| 907 | .gm-anim-spinner { | ||
| 908 | -fx-fill: -gm-ink2; | ||
| 909 | } | ||
| 910 | |||
| 911 | .gm-anim-spinner-hub { | ||
| 912 | -fx-fill: -gm-panel; | ||
| 913 | -fx-stroke: -gm-ink2; | ||
| 914 | -fx-stroke-width: 1; | ||
| 915 | } | ||
| 916 | |||
| 917 | .gm-btn-icon:selected .gm-anim-spinner { | ||
| 918 | -fx-fill: -gm-accent; | ||
| 919 | } | ||
| 920 | |||
| 921 | .gm-btn-icon:selected .gm-anim-spinner-hub { | ||
| 922 | -fx-fill: -gm-panel; | ||
| 923 | -fx-stroke: -gm-accent; | ||
| 924 | -fx-stroke-width: 1; | ||
| 925 | } | ||
| 926 | |||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/trade_history_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/trade_history_item.fxml new file mode 100644 index 0000000..2a2bf92 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/trade_history_item.fxml | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Priority?> | ||
| 7 | |||
| 8 | <fx:root type="javafx.scene.layout.HBox" alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-divider-top" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 9 | <padding> | ||
| 10 | <Insets bottom="7.0" left="11.0" right="11.0" top="7.0" /> | ||
| 11 | </padding> | ||
| 12 | <children> | ||
| 13 | <Label fx:id="numLabel" minWidth="66.0" prefWidth="66.0" styleClass="gm-text-sm, gm-font-semibold, gm-text-ink2, gm-font-mono" /> | ||
| 14 | <Label fx:id="userLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-sm, gm-font-medium, gm-text-ink" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 15 | <Label fx:id="optionLabel" minWidth="76.0" prefWidth="76.0" styleClass="gm-text-sm, gm-font-bold" /> | ||
| 16 | <Label fx:id="sharesLabel" alignment="CENTER_RIGHT" minWidth="92.0" prefWidth="92.0" styleClass="gm-text-sm, gm-font-mono" /> | ||
| 17 | <Label fx:id="paidLabel" alignment="CENTER_RIGHT" minWidth="88.0" prefWidth="88.0" styleClass="gm-text-sm, gm-font-semibold, gm-font-mono, gm-text-accent" /> | ||
| 18 | </children> | ||
| 19 | </fx:root> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/user_event_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/user_event_item.fxml new file mode 100644 index 0000000..93631a1 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/user_event_item.fxml | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Priority?> | ||
| 7 | |||
| 8 | <fx:root type="javafx.scene.layout.HBox" alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-participation-row" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 9 | <padding> | ||
| 10 | <Insets bottom="8.0" left="11.0" right="11.0" top="8.0" /> | ||
| 11 | </padding> | ||
| 12 | <children> | ||
| 13 | <Label fx:id="nameLabel" maxWidth="1.7976931348623157E308" minWidth="0.0" styleClass="gm-text-sm, gm-font-medium, gm-text-ink" textOverrun="ELLIPSIS" HBox.hgrow="ALWAYS" /> | ||
| 14 | <!-- Sized for the longest values ("Market Maker", "Order Book") in every skin's mono font; never shrinks. --> | ||
| 15 | <Label fx:id="roleLabel" minWidth="-Infinity" prefWidth="92.0" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 16 | <Label fx:id="typeLabel" minWidth="-Infinity" prefWidth="78.0" styleClass="gm-text-sm, gm-font-regular, gm-text-ink2" /> | ||
| 17 | <Label fx:id="yesLabel" alignment="CENTER_RIGHT" minWidth="78.0" prefWidth="78.0" styleClass="gm-text-sm, gm-font-mono" /> | ||
| 18 | <Label fx:id="noLabel" alignment="CENTER_RIGHT" minWidth="78.0" prefWidth="78.0" styleClass="gm-text-sm, gm-font-mono" /> | ||
| 19 | <Label fx:id="plLabel" alignment="CENTER_RIGHT" minWidth="90.0" prefWidth="90.0" styleClass="gm-pnl-cell" /> | ||
| 20 | </children> | ||
| 21 | </fx:root> | ||
diff --git a/ui-desktop/src/main/resources/market/guess/ui/desktop/user_list_item.fxml b/ui-desktop/src/main/resources/market/guess/ui/desktop/user_list_item.fxml new file mode 100644 index 0000000..dec7706 --- /dev/null +++ b/ui-desktop/src/main/resources/market/guess/ui/desktop/user_list_item.fxml | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | |||
| 3 | <?import javafx.geometry.Insets?> | ||
| 4 | <?import javafx.scene.control.Label?> | ||
| 5 | <?import javafx.scene.layout.HBox?> | ||
| 6 | <?import javafx.scene.layout.Priority?> | ||
| 7 | <?import javafx.scene.layout.VBox?> | ||
| 8 | |||
| 9 | <fx:root type="javafx.scene.layout.HBox" alignment="CENTER_LEFT" spacing="8.0" styleClass="gm-list-row" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"> | ||
| 10 | <padding> | ||
| 11 | <Insets bottom="9.0" left="11.0" right="11.0" top="9.0" /> | ||
| 12 | </padding> | ||
| 13 | <children> | ||
| 14 | <VBox maxWidth="1.7976931348623157E308" minWidth="0.0" spacing="3.0" HBox.hgrow="ALWAYS"> | ||
| 15 | <children> | ||
| 16 | <Label fx:id="nameLabel" styleClass="gm-text-md, gm-font-semibold, gm-text-ink" /> | ||
| 17 | <Label fx:id="roleLabel" styleClass="gm-text-xs, gm-font-regular, gm-text-ink2" /> | ||
| 18 | </children> | ||
| 19 | </VBox> | ||
| 20 | <Label fx:id="balanceLabel" alignment="CENTER_RIGHT" minWidth="84.0" prefWidth="84.0" styleClass="gm-balance-cell" /> | ||
| 21 | </children> | ||
| 22 | </fx:root> | ||