diff options
Diffstat (limited to 'ui-desktop/src/main')
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> | ||