Compare commits

...

25 commits

Author SHA1 Message Date
tooomm
6150eb43e8 use new variable for added test to master, too 2026-09-12 20:38:00 +02:00
tooomm
6d7dadf657
Merge branch 'master' into tooomm-qt5 2026-09-12 18:34:48 +02:00
tooomm
5d025ca0bd
Use capitalized app names (#7255)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* use capitalized app name

* update urls

* app description

* Update main.cpp
2026-09-12 17:30:46 +02:00
BruebachL
a85203e457
Migrate theme asset loads to scheme-variant resolution (#7209-2) (#7276)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Replace remaining QPixmap("theme:...") call sites with themePixmap() so icons, replay controls, card backs, and other images resolve to -dark/-light variants under theme schemes. Stem-exact 1:1 migration; behavior unchanged for non-variant themes.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-11 17:59:57 +02:00
BruebachL
e9bf1e6e46
[Theme] Add scheme-variant asset resolution (#7275)
* Add scheme-variant theme asset resolution (#7209-1)

ThemeManager::assetPath() and schemeVariantPath() resolve a theme asset to its scheme-variant file (prefix-light/dark.png) with fallback to the plain asset, and themePixmap()/loadBrush()/loadExtraBrush() use them. CSS files load style-dark.css or style-light.css when present. Home widget re-resolves its background on theme change.

Link pixel_map_generator.cpp into the oracle target, which needs Qt6::Xml for QDomDocument.

* Fix clang-format wrap of theme format probe lists

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-11 17:59:57 +02:00
BruebachL
202a5ac958
[Client/Server/Protocol] Surface live metrics in the Developer tab (#7212)
* [Server] Instrument command processing, game starts, and event loops

Add a lock-free MetricsRegistry that accumulates per-command processing
times in preallocated histogram slots (one per protobuf command type,
bucketed at 1/5/10/25/50/100/250/500/1000/2500/5000 ms +Inf). The
hot-path observeCommand() uses only relaxed atomic adds — no locks,
no allocations, no cache-line ping-pong beyond the unavoidable counter
updates.

Wire the registry into AbstractServerSocketInterface::processCommandContainer()
so every processed command is attributed with its container's wall-clock
time. When a container exceeds metrics/slow_command_ms (default 500),
a warning is logged including the connected username.

Add an EventLoopWatchdog heartbeat that runs on every socket pool thread.
If a heartbeat overshoots metrics/stall_warn_ms (default 2000 ms), the
overshoot is recorded in atomic counters and a warning is logged. Both
thresholds are configurable in servatrice.ini; setting stall_warn_ms to 0
disables the watchdogs entirely.

Track game-start durations via a separate histogram in MetricsRegistry.
Server_Game::startGameNow() measures the time from zone creation through
player materialization and reports it via Server::observeGameStartDurationMs().

Add a live card-count gauge: Server_Game exposes getCardsInGame() and
Servatrice::getCardsInGamesTotal() sums across all running games under
the appropriate read locks.

Include a standalone metrics_registry_test (Google Test) that validates
empty registries, single/multi-sample histograms, kind encoding,
overflow-slot collapse, negative-duration clamping, gauge rendering,
and the game-start histogram separation.

Took 10 minutes

* [Client/Server/Protocol] Surface live metrics in the Developer tab

Extend Response_GetServerStats with live counters from the in-process
MetricsRegistry: cards in games, event loop stall totals/worst,
total commands processed, average command time, active command types,
and game-start count/duration. Add a repeated CommandStats message
carrying per-command breakdowns (kind, extension number, resolved
protobuf name, count, total ms) for every type that has seen at
least one sample.

Server-side cmdGetServerStats() populates all new fields after the
existing DB uptime snapshot query, resolving protobuf extension names
via the descriptor pool for human-readable labels like
session/Command_Ping.

Expand TabDeveloper with two tables: an overview section (existing
DB stats plus the new live metrics) and a per-command breakdown table
(Command / Count / Total ms / Avg ms) sorted by total_ms descending
so the hottest commands surface first.

Took 55 minutes

Took 47 seconds

* [Server] Drop dead Prometheus histogram, add developer command metrics, fix watchdog init order

- metrics_registry: remove toPrometheusText/appendCumulativeBuckets and the time-bucket histogram that nothing in production ever emitted (the future /metrics exporter can bring it back); keep counts/totals read by the Developer tab
- Fix +Inf bucket routing that never incremented, and its test that locked the bug in
- Instrument developer_command container (kind 6) in processCommandContainer and stats label resolution
- Read metrics/{slow_command_ms,stall_warn_ms} at the top of initServer() so stall_warn_ms=0 disables the watchdogs before pool threads start
- Shrink KindStride to 1280 (largest extension in use is 1206) with a static_assert; document scrape cost of getCardsInGamesTotal; note slow_command logging has no rate limit in servatrice.ini.example

* [Tests] Give metrics_registry_test an explicit main

* [Server] Record only the dispatched command family; drop unused totals

processCommandContainer recorded every family in a container even though
the base if/else-if dispatch processes at most one. An unauthenticated
client could batch a session command (login) with fabricated developer,
moderator, and admin entries and forge genuine-looking samples that were
never executed or authorized. Mirror the base's selection, skip when the
handler was already deleted, and skip entries whose extension number is
-1 (which would otherwise wrap into the previous kind's id range).

[Server] Drop dead process-lifetime byte/uptime counters

txBytesTotal/rxBytesTotal added an atomic RMW to every socket write and
read for counters nothing consumes (cmdGetServerStats fills tx_bytes,
rx_bytes, and uptime_secs from the DB snapshot). Remove the two atomics
and the getTxBytesTotal/getRxBytesTotal/getUptimeSeconds getters; the
incTxBytes/incRxBytes slots and mutexes remain for the ISL legacy
counters.

[Protocol] Document kind 5 as developer in CommandStats

NumKinds is 6 and the server emits kind_index = 5 for developer
commands; the comment stopped at 4.

* [Client] Togglable auto-refresh for Developer stats tab

* [Oracle] Fix clang-format alignment of card type priority list

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-11 17:18:56 +02:00
BruebachL
d5d99e4dfb
[Server/Client/Protocol] Add developer staff role (#7211)
* [Server/Client/Protocol] Add developer staff role

Introduce a Developer staff level (proto flag 32, DB admin bit 8) that
sits between admin and moderator: no kick/ban/warn/report/admin powers,
but gets server log access via a new developer command container family
(GET_SERVER_STATS, VIEWLOG_HISTORY) and an idle-timeout exemption.

- Protocol: IsDeveloper flag, developer_commands.proto envelope,
  Command_GetServerStats/Command_GetLogHistory, Response_GetServerStats,
  Command_AdjustMod.should_be_developer
- Servatrice: fail-closed developer dispatcher, uptime snapshot handler,
  shared log history handler reuse, bit-8 DB mapping
- Client: burgundy pawn/badge/labels/sort order, prepareDeveloperCommand,
  minimal Developer stats tab, log tab access, promote/demote actions

Took 24 minutes

Took 18 seconds

* [Server/Client/Protocol] Address developer role review feedback

Address ZeizaZach's review of the developer staff role:

- Nudge the developer log query to exclude private chat and sender IPs
  (the ModeratorCommand path still sees everything).
- Deduplicate Command_GetLogHistory into Command_ViewLogHistory, which now
  extends both ModeratorCommand (ext) and DeveloperCommand (dev_ext); the
  client picks the DeveloperCommand-scoped extension by extendee, and the
  server reads it via the extension number.
- Pull the uptime snapshot SQL into Servatrice_DatabaseInterface as
  getLatestUptimeSnapshot() and widen the reported counters to 64-bit.
- Document the admin bitfield (1 admin, 2 moderator, 4 judge, 8 developer)
  and add a server-side test for the developer command path.

* Add missing trailing newline to user_context_menu.cpp

* Remove stale includes of deleted command_get_log_history proto

The Command_GetLogHistory message was folded into Command_ViewLogHistory,
which deleted command_get_log_history.proto, but serversocketinterface
still #included its generated header. Fresh CI builds fail on the missing
file; local builds masked it by reusing a previously generated header.

* [Server] Exclude chat rows when private-chat filter is bypassable

A developer who omits log_location entirely — or sends only "chat" —
leaves chatType, gameType, roomType all false, so getMessageLogHistory
skips the target_type clause and returns every row, private messages
included. When !allowPrivateChat the server now forces game+room when
no surviving location was requested, guaranteeing the query always
carries a target_type restriction.

[Client] Demote mod+dev to moderator path in log-tab dispatch

The developer command family is strictly weaker than the moderator one
(no private chat, no sender_ip, ip filter ignored), so granting the
developer bit to an existing moderator must not silently strip their
capabilities. useDeveloperCommands is now true only when the user holds
the developer bit and not the moderator bit.

[Client] Hide the IP-address filter for developer log tab users

The developer path ignores the ip_address query field server-side.
Showing the field lets a developer type an IP and get results that are
silently unfiltered by it rather than an empty result set — reads as a
broken filter. Hide labelFindIPAddress/findIPAddress alongside the
privateChat checkbox.

* Developer pawn is silver.

* [Client] Fix indentation of merged Card Art Rules / Developer tabs

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-11 17:18:56 +02:00
BruebachL
7d867b9745
[Oracle/Client] Report card database download progress (#7253)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [Oracle/Client] Report card database download progress

Card database updates ran invisibly: MTGJSON parsing spun an indeterminate
bar on the UI thread and the set import blocked the window, while the
onboarding wizard spawned `oracle -b` with no progress to show at all.

- Add byte-level scan progress to `RawJson::scanSetRanges` via an optional
  callback, throttled to ~100 reports per scan.
- Emit `OracleImporter::dataReadProgress` during the scan and import sets on a
  worker thread, driving the wizard's progress bar per set.
- With `-b`, write machine-readable `PROGRESS <stage> <done> <total>` lines to
  stdout for the download/scan/import stages; stderr keeps the log output.
- Parse the oracle stdout in `MainWindow` and forward it to the onboarding
  wizard, giving the card database step a determinate bar with stage-specific
  status text.
- Guard the async workers against the wizard being closed mid-run.
- Add Google Test coverage for scan progress reporting.

* [Oracle/Client] Harden oracle progress workers and quit prompt

Address review feedback on the download-progress change: decompress and read
sets files off the UI thread, cancel the load/import workers before the
wizard can tear down the importer, and show an 'Extracting file...' status
plus a clean 100% tail so the poll never looks stuck. Quitting Cockatrice
while a card database update runs now asks for confirmation.

* Show 100% for 500ms on complete.

* Disable buttons on set import until done.

* Clean up progress bar.

* Drop wrapper around lambda

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-09 22:27:16 +02:00
BruebachL
69e8f80fa1
[Oracle] Prefer higher-priority maintype when merging split cards (#7272)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [Oracle] Prefer higher-priority maintype when merging split cards

Adventure cards (e.g. Bonecrusher Giant, Virtue of Knowledge) are stored as
split cards in MTGJSON, with a Creature/permanent face and an Instant/Sorcery
adventure face. When the two faces are merged into a single card, the code
previously discarded the second face's maintype entirely, keeping whichever
face was processed first.

If the Instant/Sorcery adventure face appeared first, the merged card got
maintype 'Instant' with tableRow 3. At runtime this made double-clicking the
card on the stack send it to the graveyard instead of the table
(PlayerActions::playCard).

Fix the merge to follow the same priority order used by getMainCardType()
(Planeswalker > Creature > Land > Sorcery > Instant > Artifact > Enchantment),
so the permanent Creature type wins for adventure cards regardless of face
order, matching the physical card and the reported expectations.

Closes #4394

* Extract helper.

* Simplify const

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 21:22:53 +02:00
BruebachL
df3defa890
[PictureLoader] Leave failed pixmap null so solid color is shown instead. (#7274)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 13:37:19 +02:00
BruebachL
030e4f5871
[Game] Stop duplicate attach log entries (#7273)
Fix duplicate attach messages in the game log caused by a redundant
child arrow in CardItem::drawAttachArrow.

Unlike the sibling drawArrow, drawAttachArrow omitted the ``card ==
this`` guard when iterating selectedItems(). Because right-clicking a
card to open the attach menu selects that card, it was always present
in selectedItems(), producing a second arrow for the same source card.

On release both arrows sent an identical Command_AttachCard, so the
server broadcast two Event_AttachCard messages and the log rendered
"attaches to" twice. Mirror the drawArrow skip condition so the active
card is excluded and only one attach command is sent.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 13:37:13 +02:00
RickyRister
6e5c58069b
[Settings] Make chat settings page scrollable (#7271)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
2026-09-07 00:52:06 -07:00
BruebachL
ebeae48652
[Game] Preserve hand card order when restoring connection (#7266)
When a player restores connection to a game, the client rebuilds each
zone from the cards the server sends in the game state. Non-coordinate
zones (hand, piles, stack) report x == 0 on every card, so inserting
each rebuilt card at that coordinate reversed the received order one
card at a time.

Append rebuilt cards in the order they arrive for zones without
coordinates; coordinate-based zones (table) keep using x/y.

Fixes #2759

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 08:21:44 +02:00
BruebachL
36f998e466
[Client] Fix pawn avatar cache key collision for players without a custom avatar (#4086) (#7264)
Player pawns are looked up in QPixmapCache under a key built from the
rendered size, user level, and the avatar pixmap's cacheKey(). A null
pixmap reports cacheKey() 0, so all players without a custom avatar
collided: the first pawn rendered for a given size and user level was
reused for the next one, showing the wrong player's pawn.

Extend the key with the rendered height, the lowercased privlevel
(matching UserLevelPixmapGenerator), and both pawn colors so that every
visually distinct pawn gets its own cache entry.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 08:21:26 +02:00
BruebachL
760fe88fa3
[Client] Add setting to ignore all private messages (#7260)
* [Client] Add setting to ignore all private messages (#1250)

* Early return

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 08:20:46 +02:00
BruebachL
fff506dbbe
[Client] Show confirmation when adding a user to the ignore list (#1875) (#7261)
Nothing in the UI confirmed that a user had been added to the ignore
list, so the action felt ambiguous and could be repeated by accident.

Show an information dialog when the server acknowledges the
add-to-ignore command.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 08:14:39 +02:00
BruebachL
8e0bdafb14
[Client] Prevent contacting users on the ignore list (#7262)
* [Client] Prevent contacting users on the ignore list (#1249)

The ignore list silences incoming messages, but a user on it was still
reachable: the context menu's chat item stayed enabled, private messages
could be sent, and a chat tab could be opened for an ignored user.

Make ignored users uncontactable: disable the chat item for them, refuse
to deliver messages typed in an open PM tab with one, and refuse to open
a new private chat tab with an ignored user (with a hint on how to undo
the ignore).

* Update cockatrice/src/interface/widgets/tabs/tab_message.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2026-09-07 08:14:15 +02:00
BruebachL
9b0d62c152
Close all zone views of a leaving player to avoid crashing (#4298) (#7270)
When a player left, GameScene::removePlayer iterated zoneViews while close()
synchronously removed the current view from that list. A judge with several open
views of the departing player (e.g. library and hand) only had the first one
closed; the remaining views were skipped and left pointing at a player that was
about to be deleted, crashing the client on the next access.

- GameScene::removePlayer: iterate over a copy of zoneViews
- GameScene::toggleZoneView: same fix for the identical iteration bug

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 02:51:38 +02:00
BruebachL
6cdeb0c428
[Client] Explain the buddy and ignore lists in the user tab (#2072) (#7258)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 02:29:17 +02:00
BruebachL
745e94f332
[Replay] Skip damage animations when skipping backward in replays (#7249)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 02:26:52 +02:00
BruebachL
b01e107908
[Client] Show horizontal art for plane and siege cards in the profile banner (#7118) (#7250)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Use the landscape orientation flag to rotate sideways-layout card art
upright before cropping, so planes/sieges show their horizontal art as
the server profile banner card instead of a rotated full card.

- Curve cropCardArt around the card's landscapeOrientation flag with
  landscape-specific art margins (mirrors CardInfoPictureWidget)
- Add shared CardArtUtils::rotateSidewaysLayoutArt helper and apply it
  to the playmat (game render and settings preview) and card info widget,
  replacing three duplicate 90-degree rotation blocks

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 02:20:54 +02:00
BruebachL
8d30ac54f0
[Game] Prevent spectator duplication when replaying joined events (#7248)
* [Game] Prevent spectator duplication when replaying joined events

The spectator branch of eventJoin emitted spectatorJoined unconditionally
even when the spectator was already present (e.g. replayed during a rewind).
Guard it like the player branch and eventGameStateChanged, and make
PlayerListWidget::addPlayer idempotent as defense in depth.

* In resetChatAndPhase() (the rewound() handler), also clear all spectators from both PlayerManager and PlayerListWidget before the replay rebuilds from event 0. The forward replay then re-adds exactly the spectators whose join events fall within the new time range via eventGameStateChanged/eventJoin.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 01:57:35 +02:00
tooomm
fb482f037e
550 MB --> 600 MB (#7257) 2026-09-06 23:13:34 +02:00
tooomm
0f0e46a177
[Oracle] Have space between size value and unit (#7254) 2026-09-06 22:19:13 +02:00
tooomm
2fe59d6326
fix table formatting (#7256) 2026-09-06 22:18:36 +02:00
149 changed files with 2962 additions and 423 deletions

View file

@ -152,7 +152,7 @@ jobs:
env: env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
CCACHE_EVICTION_AGE: 7d CCACHE_EVICTION_AGE: 7d
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CMAKE_GENERATOR: 'Ninja' CMAKE_GENERATOR: 'Ninja'
NAME: ${{ matrix.distro }}${{ matrix.version }} NAME: ${{ matrix.distro }}${{ matrix.version }}
@ -342,7 +342,7 @@ jobs:
timeout-minutes: 100 timeout-minutes: 100
env: env:
CCACHE_DIR: ${{ github.workspace }}/.cache/ CCACHE_DIR: ${{ github.workspace }}/.cache/
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
steps: steps:
- name: "Checkout" - name: "Checkout"

View file

@ -149,15 +149,15 @@ You can then
<br> <br>
The following flags (with their non-default values) can be passed to `cmake`: The following flags (with their non-default values) can be passed to `cmake`:
| Flag | Description | | Flag | Description |
| --- | --- | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server | | `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client | | `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool | | `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings | | `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | | `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files | | `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available | | `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
# Run # Run

View file

@ -37,7 +37,7 @@ set(QT_COMPONENTS_COCKATRICE
QuickWidgets QuickWidgets
) )
set(QT_COMPONENTS_ORACLE Concurrent Network Svg Widgets) set(QT_COMPONENTS_ORACLE Concurrent Network Svg Widgets Xml)
set(QT_COMPONENTS_SERVATRICE Network Sql WebSockets) set(QT_COMPONENTS_SERVATRICE Network Sql WebSockets)

View file

@ -166,6 +166,7 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.cpp src/interface/widgets/cards/art_crop_attribution.cpp
src/interface/widgets/cards/card_art_utils.cpp
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
@ -381,6 +382,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp src/interface/widgets/tabs/tab_deck_storage.cpp
src/interface/widgets/tabs/tab_developer.cpp
src/interface/widgets/tabs/tab_game.cpp src/interface/widgets/tabs/tab_game.cpp
src/interface/widgets/tabs/tab_home.cpp src/interface/widgets/tabs/tab_home.cpp
src/interface/widgets/tabs/tab_logs.cpp src/interface/widgets/tabs/tab_logs.cpp

View file

@ -1,5 +1,6 @@
#include "remote_connection_controller.h" #include "remote_connection_controller.h"
#include "../../../interface/pixel_map_generator.h"
#include "../../settings/cache_settings.h" #include "../../settings/cache_settings.h"
#include "../interface/widgets/dialogs/dlg_connect.h" #include "../interface/widgets/dialogs/dlg_connect.h"
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h" #include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
@ -180,7 +181,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
"games will be lost.\nReason for shutdown: %1", "games will be lost.\nReason for shutdown: %1",
"", event.minutes()) "", event.minutes())
.arg(QString::fromStdString(event.reason()))); .arg(QString::fromStdString(event.reason())));
serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64)); serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
serverShutdownMessageBox.setText(tr("Scheduled server shutdown")); serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal); serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true); serverShutdownMessageBox.setVisible(true);

View file

@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
QDir dir; QDir dir;
availableThemes.clear(); availableThemes.clear();
// load themes from user profile dir // Load themes from user profile dir
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds"); dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
} }
} }
// load themes from cockatrice system dir // Load themes from Cockatrice system dir
dir.setPath(qApp->applicationDirPath() + dir.setPath(qApp->applicationDirPath() +
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
"/../Resources/sounds" "/../Resources/sounds"

View file

@ -1,5 +1,6 @@
#include "filter_builder.h" #include "filter_builder.h"
#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/custom_line_edit.h"
#include <QComboBox> #include <QComboBox>
@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i)); typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
} }
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString()); QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
ok->setObjectName("ok"); ok->setObjectName("ok");
ok->setMaximumSize(20, 20); ok->setMaximumSize(20, 20);

View file

@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent); convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
} }
void CounterState::setValue(int newValue) void CounterState::setValue(int newValue, bool skipDamageAnimation)
{ {
if (newValue == value) { if (newValue == value) {
return; return;
} }
int old = value; int old = value;
value = newValue; value = newValue;
emit valueChanged(old, newValue); emit valueChanged(old, newValue, skipDamageAnimation);
} }

View file

@ -35,10 +35,23 @@ public:
return value; return value;
} }
void setValue(int newValue); /**
* @brief Set the counter value.
* @param newValue The new value.
* @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
* suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
* rewinds.
*/
void setValue(int newValue, bool skipDamageAnimation = false);
signals: signals:
void valueChanged(int oldValue, int newValue); /**
* @brief Emitted whenever the value changes.
* @param oldValue The previous value.
* @param newValue The new value.
* @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
*/
void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
private: private:
int id; int id;

View file

@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
QString playerName = QString::fromStdString(playerInfo.user_info().name()); QString playerName = QString::fromStdString(playerInfo.user_info().name());
emit addPlayerToAutoCompleteList(playerName); emit addPlayerToAutoCompleteList(playerName);
if (game->getPlayerManager()->getPlayers().contains(playerId)) { PlayerManager *playerManager = game->getPlayerManager();
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
return; return;
} }
if (playerInfo.spectator()) { if (playerInfo.spectator()) {
game->getPlayerManager()->addSpectator(playerId, playerInfo); playerManager->addSpectator(playerId, playerInfo);
emit logJoinSpectator(playerName); emit logJoinSpectator(playerName);
emit spectatorJoined(playerInfo); emit spectatorJoined(playerInfo);
} else { } else {

View file

@ -13,7 +13,8 @@
enum EventProcessingOption enum EventProcessingOption
{ {
SKIP_REVEAL_WINDOW = 0x0001, SKIP_REVEAL_WINDOW = 0x0001,
SKIP_TAP_ANIMATION = 0x0002 SKIP_TAP_ANIMATION = 0x0002,
SKIP_DAMAGE_ANIMATION = 0x0004
}; };
// Wrap it in a QFlags typedef // Wrap it in a QFlags typedef

View file

@ -262,14 +262,15 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
player->addCounter(event.counter_info()); player->addCounter(event.counter_info());
} }
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event) void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options)
{ {
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr); CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
if (!ctr) { if (!ctr) {
return; return;
} }
int oldValue = ctr->getValue(); int oldValue = ctr->getValue();
ctr->setValue(event.value()); const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION);
ctr->setValue(event.value(), skipDamageAnimation);
emit logSetCounter(player, ctr->getName(), event.value(), oldValue); emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
} }
@ -625,7 +626,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext)); eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
break; break;
case GameEvent::SET_COUNTER: case GameEvent::SET_COUNTER:
eventSetCounter(event.GetExtension(Event_SetCounter::ext)); eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
break; break;
case GameEvent::DEL_COUNTER: case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext)); eventDelCounter(event.GetExtension(Event_DelCounter::ext));

View file

@ -153,7 +153,7 @@ public:
void eventCreateCounter(const Event_CreateCounter &event); void eventCreateCounter(const Event_CreateCounter &event);
/// Set a player-level counter value. /// Set a player-level counter value.
void eventSetCounter(const Event_SetCounter &event); void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options);
/// Delete a player-level counter. /// Delete a player-level counter.
void eventDelCounter(const Event_DelCounter &event); void eventDelCounter(const Event_DelCounter &event);

View file

@ -175,7 +175,15 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
auto *card = new CardItem(this); auto *card = new CardItem(this);
card->processCardInfo(cardInfo); card->processCardInfo(cardInfo);
zone->addCard(card, false, cardInfo.x(), cardInfo.y()); // Zones without coordinates (hand, piles, stack) preserve the order
// they arrive in on the server in the positions of their cards list.
// The x coordinate of such cards is always 0, so inserting at it
// would reverse the list on reconnect. Append instead.
if (zoneInfo.with_coords()) {
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
} else {
zone->addCard(card, false, -1);
}
} }
} }
if (zoneInfo.has_always_reveal_top_card()) { if (zoneInfo.has_always_reveal_top_card()) {

View file

@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
return player; return player;
} }
void PlayerManager::clearSpectators()
{
const QList<int> spectatorIds = spectators.keys();
for (int spectatorId : spectatorIds) {
removeSpectator(spectatorId);
}
}
void PlayerManager::onPlayerConceded(int playerId, bool conceded) void PlayerManager::onPlayerConceded(int playerId, bool conceded)
{ {
// Everything else cares about this // Everything else cares about this

View file

@ -100,6 +100,9 @@ public:
emit spectatorRemoved(spectatorId, spectatorInfo); emit spectatorRemoved(spectatorId, spectatorInfo);
} }
/** @brief Remove all spectators, emitting the removal signal for each. */
void clearSpectators();
[[nodiscard]] AbstractGame *getGame() const [[nodiscard]] AbstractGame *getGame() const
{ {
return game; return game;

View file

@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
{ {
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
value = newValue; value = newValue;
onValueChanged(oldValue, newValue); onValueChanged(oldValue, newValue, skipDamageAnimation);
update(); update();
}); });
@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
setTextValue(QString::number(curValue)); setTextValue(QString::number(curValue));
} }
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/) void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/)
{ {
// Default: no feedback. Subclasses such as PlayerCounter override this to // Default: no feedback. Subclasses such as PlayerCounter override this to
// flash the counter on meaningful changes (life gain/loss). // flash the counter on meaningful changes (life gain/loss).

View file

@ -38,8 +38,9 @@ protected:
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash). * @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
* *
* Called whenever the counter's value changes, before the item repaints. * Called whenever the counter's value changes, before the item repaints.
* @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds).
*/ */
virtual void onValueChanged(int oldValue, int newValue); virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation);
void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;

View file

@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
for (const auto &item : scene()->selectedItems()) { for (const auto &item : scene()->selectedItems()) {
CardItem *card = qgraphicsitem_cast<CardItem *>(item); CardItem *card = qgraphicsitem_cast<CardItem *>(item);
if (card == nullptr) { if (card == nullptr || card == this) {
continue; continue;
} }
if (card->getZone() != state->getZone()) { if (card->getZone() != state->getZone()) {

View file

@ -221,7 +221,12 @@ void GameScene::removePlayer(PlayerLogic *player)
clearArrowsForPlayer(player->getPlayerInfo()->getId()); clearArrowsForPlayer(player->getPlayerInfo()->getId());
for (ZoneViewWidget *zone : zoneViews) { // Closing a view removes it from zoneViews synchronously, so iterate over a
// copy: otherwise a player with several open views (e.g. library and hand)
// only has the first one closed here and the remaining views are left
// pointing at a player that is about to be deleted.
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
for (ZoneViewWidget *zone : zoneViewCopy) {
if (zone->getPlayer() == player) { if (zone->getPlayer() == player) {
zone->close(); zone->close();
} }
@ -664,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList<QGraphicsItem *> &items,
*/ */
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed) void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
{ {
for (auto &view : zoneViews) { // Closing a view removes it from zoneViews synchronously, so iterate over a
// copy to make sure every already-open matching view is closed.
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
for (auto *view : zoneViewCopy) {
ZoneViewZone *temp = view->getZone(); ZoneViewZone *temp = view->getZone();
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player && if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) { qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {

View file

@ -1,5 +1,6 @@
#include "hand_counter.h" #include "hand_counter.h"
#include "../interface/pixel_map_generator.h"
#include "zones/card_zone.h" #include "zones/card_zone.h"
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
@ -32,7 +33,8 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize(); QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
QPixmap cachedPixmap; QPixmap cachedPixmap;
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) { if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); cachedPixmap =
themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap); QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
} }
resetPainterTransform(painter); resetPainterTransform(painter);

View file

@ -3,6 +3,7 @@
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../interface/card_picture_loader/card_picture_loader.h" #include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/widgets/cards/art_crop_attribution.h" #include "../../interface/widgets/cards/art_crop_attribution.h"
#include "../../interface/widgets/cards/card_art_utils.h"
#include "../../interface/widgets/playmat/playmat_utils.h" #include "../../interface/widgets/playmat/playmat_utils.h"
#include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h" #include "../board/abstract_card_item.h"
@ -251,8 +252,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
AbstractCounter *widget; AbstractCounter *widget;
if (state->getName() == "life") { if (state->getName() == "life") {
widget = playerTarget->addCounter(state); widget = playerTarget->addCounter(state);
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
if (newValue < oldValue) { if (newValue < oldValue && !skipDamageAnimation) {
tableZoneGraphicsItem->triggerDamageShimmer(); tableZoneGraphicsItem->triggerDamageShimmer();
} }
}); });
@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
hasPlaymat = true; hasPlaymat = true;
emit playmatChanged(true); emit playmatChanged(true);
} }
playmatPixmap = fullRes; playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
update(); update();
} }

View file

@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent) QWidget *parent)
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false) : QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
{ {
readyIcon = QPixmap("theme:icons/ready_start"); readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
notReadyIcon = QPixmap("theme:icons/not_ready_start"); notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
concededIcon = QPixmap("theme:icons/conceded"); concededIcon = themePixmap(QStringLiteral("icons/conceded"));
playerIcon = loadColorAdjustedPixmap("theme:icons/player"); playerIcon = loadColorAdjustedPixmap("theme:icons/player");
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales"); judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator"); spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
lockIcon = QPixmap("theme:icons/lock"); lockIcon = themePixmap(QStringLiteral("icons/lock"));
if (tabSupervisor) { if (tabSupervisor) {
itemDelegate = new PlayerListItemDelegate(this); itemDelegate = new PlayerListItemDelegate(this);
@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player) void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
{ {
if (players.contains(player.player_id())) {
updatePlayerProperties(player);
return;
}
QTreeWidgetItem *newPlayer = new PlayerListTWI; QTreeWidgetItem *newPlayer = new PlayerListTWI;
players.insert(player.player_id(), newPlayer); players.insert(player.player_id(), newPlayer);
updatePlayerProperties(player); updatePlayerProperties(player);
@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
delete takeTopLevelItem(indexOfTopLevelItem(player)); delete takeTopLevelItem(indexOfTopLevelItem(player));
} }
void PlayerListWidget::clearSpectators()
{
const QList<int> playerIds = players.keys();
for (int playerId : playerIds) {
QTreeWidgetItem *player = players.value(playerId, 0);
if (player && !player->data(1, Qt::UserRole).toBool()) {
removePlayer(playerId);
}
}
}
void PlayerListWidget::setActivePlayer(int playerId) void PlayerListWidget::setActivePlayer(int playerId)
{ {
QMapIterator<int, QTreeWidgetItem *> i(players); QMapIterator<int, QTreeWidgetItem *> i(players);

View file

@ -66,6 +66,7 @@ public slots:
void addPlayer(const ServerInfo_PlayerProperties &player); void addPlayer(const ServerInfo_PlayerProperties &player);
void removePlayer(int playerId); void removePlayer(int playerId);
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1); void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
void clearSpectators();
}; };
#endif #endif

View file

@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
} }
} }
void PlayerCounter::onValueChanged(int oldValue, int newValue) void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
{ {
flashDelta = newValue - oldValue; flashDelta = newValue - oldValue;
if (flashDelta == 0) { if (flashDelta == 0) {
@ -81,6 +81,11 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue)
return; return;
} }
if (skipDamageAnimation) {
flashAlpha = 0.0;
return;
}
flashAlpha = 1.0; flashAlpha = 1.0;
flashClock.start(); flashClock.start();
if (scene()) { if (scene()) {
@ -132,8 +137,18 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect); QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
QSize translatedSize = translatedRect.size().toSize(); QSize translatedSize = translatedRect.size().toSize();
QPixmap cachedPixmap; QPixmap cachedPixmap;
// The key must cover everything the generated pawn depends on: the rendered
// size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
// 0 for every null pixmap, so the default-pawn branch additionally needs the
// pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
// in the key — otherwise two players without a custom avatar (and the same
// user level) would share one cached pawn.
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" + const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey()); QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
"_" + QString::number(fullPixmap.cacheKey()) + "_" +
QString::fromStdString(info->privlevel()).toLower() + "_" +
QString::fromStdString(info->pawn_colors().left_side()) + "_" +
QString::fromStdString(info->pawn_colors().right_side());
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) { if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height()); cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());

View file

@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
protected: protected:
void onValueChanged(int oldValue, int newValue) override; void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
private: private:
static constexpr qreal flashDurationMs = 450.0; static constexpr qreal flashDurationMs = 450.0;

View file

@ -62,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit.setClearButtonEnabled(true); searchEdit.setClearButtonEnabled(true);
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
@ -549,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
{ {
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option); QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
if (titleBar) { if (titleBar) {
titleBar->icon = QPixmap("theme:cockatrice"); titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
} }
} }

View file

@ -1,6 +1,7 @@
#include "card_picture_loader.h" #include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../pixel_map_generator.h"
#include "card_picture_loader_cache_method.h" #include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h" #include "card_picture_loader_local_schemes.h"
@ -62,7 +63,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height()); QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
@ -83,7 +84,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height()); "_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
@ -105,7 +106,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height()); "_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
@ -138,7 +139,8 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
QPixmap bigPixmap; QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) { if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) { if (bigPixmap.isNull()) {
getCardBackLoadingFailedPixmap(pixmap, size); // Leave the pixmap null so callers fall back to a solid color
// instead of showing the card back.
QDateTime failedAtTime = getInstance().failedAt.value(key); QDateTime failedAtTime = getInstance().failedAt.value(key);
if (!failedAtTime.isValid() || if (!failedAtTime.isValid() ||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {

View file

@ -17,7 +17,7 @@ enum Format
PlainText, PlainText,
/** /**
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags. * This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
* Stored as .cod files. * Stored as .cod files.
*/ */
Cockatrice Cockatrice

View file

@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
result = deckList.loadFromFile_Native(&file); result = deckList.loadFromFile_Native(&file);
if (!result) { if (!result) {
qCInfo(DeckLoaderLog) << "Failed to load " << fileName qCInfo(DeckLoaderLog) << "Failed to load " << fileName
<< "as cockatrice format; retrying as plain format"; << "as Cockatrice format; retrying as plain format";
file.seek(0); file.seek(0);
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
fmt = DeckFileFormat::PlainText; fmt = DeckFileFormat::PlainText;

View file

@ -131,7 +131,7 @@ public:
static void printDeckList(QPrinter *printer, const DeckList &deckList); static void printDeckList(QPrinter *printer, const DeckList &deckList);
/** /**
* Converts the given deck's file to the cockatrice file format. * Converts the given deck's file to the Cockatrice file format.
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to. * Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo. * @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
* @return Whether the conversion succeeded. * @return Whether the conversion succeeded.

View file

@ -1,5 +1,7 @@
#include "pixel_map_generator.h" #include "pixel_map_generator.h"
#include "theme_manager.h"
#include <QApplication> #include <QApplication>
#include <QDomDocument> #include <QDomDocument>
#include <QFile> #include <QFile>
@ -14,6 +16,7 @@
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff"; #define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701"; #define DEFAULT_COLOR_ADMIN "#ff2701";
#define DEFAULT_COLOR_DEVELOPER "#B8B8B8"
/** /**
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size. * Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
@ -82,7 +85,13 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
/** /**
* Try to load path image from non-SVG formats, otherwise fall back to SVG. * Try to load path image from non-SVG formats, otherwise fall back to SVG.
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading. * This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
* @param path The path to the file, with no file extension. File formats will be automatically detected. *
* The path may already carry the resolved file extension (e.g. via
* ThemeManager::assetPath); such paths are loaded directly. Otherwise a
* format-agnostic lookup probes png, jpg and finally svg.
*
* @param path The path to the file, with no file extension unless the caller
* already resolved it. File formats will be automatically detected.
* @param size The desired size of the pixmap. * @param size The desired size of the pixmap.
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG). * @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
* *
@ -90,6 +99,19 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
*/ */
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false) static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
{ {
if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) {
return loadSvg(path, size, expandOnly);
}
if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) ||
path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) {
QPixmap pix(path);
if (!pix.isNull()) {
return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return {};
}
const auto formats = {"png", "jpg"}; const auto formats = {"png", "jpg"};
QPixmap returnPixmap; QPixmap returnPixmap;
@ -111,7 +133,8 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
return pmCache.value(key); return pmCache.value(key);
} }
QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height)); QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name),
QSize(height, height));
pmCache.insert(key, pixmap); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
@ -359,6 +382,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN; colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
colorLeft = DEFAULT_COLOR_DEVELOPER;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT; colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT; colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;
@ -396,7 +421,8 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
return pmCache.value(key); return pmCache.value(key);
} }
QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true); QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")),
QSize(height, height), true);
pmCache.insert(key, pixmap); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
} }
@ -411,7 +437,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
} }
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed"; QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true); QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name),
QSize(height, height), true);
pmCache.insert(key, pixmap); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
@ -472,6 +499,13 @@ QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
QPixmap loadColorAdjustedPixmap(const QString &name) QPixmap loadColorAdjustedPixmap(const QString &name)
{ {
// Prefer an authored scheme-qualified variant when one exists for this asset.
const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size()));
if (!variant.isEmpty()) {
return QPixmap(QStringLiteral("theme:") + variant);
}
// Legacy fallback: runtime-invert for dark mode when no authored variant.
if (qApp->palette().windowText().color().lightness() > 200) { if (qApp->palette().windowText().color().lightness() > 200) {
QImage img(name); QImage img(name);
img.invertPixels(); img.invertPixels();
@ -482,3 +516,21 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
return QPixmap(name); return QPixmap(name);
} }
} }
QPixmap themePixmap(QStringView prefix)
{
const QString resolved = themeManager->assetPath(prefix);
return QPixmap(QStringLiteral("theme:") + resolved);
}
void clearPixmapGeneratorCaches()
{
PhasePixmapGenerator::clear();
CounterPixmapGenerator::clear();
PingPixmapGenerator::clear();
CountryPixmapGenerator::clear();
UserLevelPixmapGenerator::clear();
LockPixmapGenerator::clear();
DropdownIconPixmapGenerator::clear();
ManaSymbolPixmapGenerator::clear();
}

View file

@ -156,4 +156,15 @@ public:
QPixmap loadColorAdjustedPixmap(const QString &name); QPixmap loadColorAdjustedPixmap(const QString &name);
// Loads a "theme:" asset (with no file extension in prefix), preferring the
// scheme-qualified variant (prefix-dark / prefix-light, resolved via
// ThemeManager::assetPath) and falling back to the plain asset. Callers load
// the returned path directly. Use for scheme-sensitive pixmaps like
// backgrounds, the card back, and the app logo.
QPixmap themePixmap(QStringView prefix);
// Clears every PixmapGenerator's static cache so scheme variants are
// re-resolved when the active theme or color scheme changes.
void clearPixmapGeneratorCaches();
#endif #endif

View file

@ -1,10 +1,12 @@
#include "theme_manager.h" #include "theme_manager.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "pixel_map_generator.h"
#include <QApplication> #include <QApplication>
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QMap> #include <QMap>
#include <QMetaEnum> #include <QMetaEnum>
@ -140,6 +142,48 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
} }
} }
QString ThemeManager::schemeVariantPath(QStringView prefix) const
{
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
QStringLiteral(".svg")};
const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light");
const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme;
for (const QString &format : formats) {
if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) {
return variantStem + format;
}
}
return QString();
}
QString ThemeManager::assetPath(QStringView prefix) const
{
// Probe order mirrors tryLoadImage: a theme may override the default SVG
// with a raster of the same stem, so raster wins over SVG within a stem.
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
QStringLiteral(".svg")};
auto findExisting = [](const QString &stem) {
for (const QString &format : formats) {
if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) {
return stem + format;
}
}
return QString();
};
// Prefer the scheme-qualified variant when it exists, else the plain
// asset as the super fallback. Both return the resolved path including
// its file extension so callers can load it directly.
const QString variant = schemeVariantPath(prefix);
if (!variant.isEmpty()) {
return variant;
}
const QString resolvedPlain = findExisting(prefix.toString());
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
}
bool ThemeManager::isBuiltInTheme() bool ThemeManager::isBuiltInTheme()
{ {
const auto themeName = SettingsCache::instance().getThemeName(); const auto themeName = SettingsCache::instance().getThemeName();
@ -180,7 +224,7 @@ QStringMap &ThemeManager::getAvailableThemes()
} }
} }
// load themes from cockatrice system dir // Load themes from Cockatrice system dir
dir.setPath(systemThemesBasePath()); dir.setPath(systemThemesBasePath());
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@ -195,7 +239,7 @@ QStringMap &ThemeManager::getAvailableThemes()
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
{ {
QBrush brush; QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName); QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) { if (tmp.isNull()) {
brush.setColor(fallbackColor); brush.setColor(fallbackColor);
brush.setStyle(Qt::SolidPattern); brush.setStyle(Qt::SolidPattern);
@ -209,7 +253,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush) QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
{ {
QBrush brush; QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName); QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) { if (tmp.isNull()) {
brush = fallbackBrush; brush = fallbackBrush;
@ -393,9 +437,19 @@ void ThemeManager::themeChangedSlot()
currentThemePath = dirPath; currentThemePath = dirPath;
QDir dir(dirPath); QDir dir(dirPath);
// CSS // CSS — prefer the scheme-qualified stylesheet (style-dark.css /
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { // style-light.css) when present, else the plain style.css as fallback.
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); if (!dirPath.isEmpty()) {
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
scheme + QStringLiteral(".css");
if (dir.exists(schemeCss)) {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
} else if (dir.exists(STYLE_CSS_NAME)) {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
} else {
qApp->setStyleSheet("");
}
} else { } else {
qApp->setStyleSheet(""); qApp->setStyleSheet("");
} }
@ -446,6 +500,7 @@ void ThemeManager::themeChangedSlot()
} }
QPixmapCache::clear(); QPixmapCache::clear();
clearPixmapGeneratorCaches();
emit themeChanged(); emit themeChanged();
} }

View file

@ -87,6 +87,20 @@ public:
// Load/save per-scheme palette colors // Load/save per-scheme palette colors
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
// Resolve prefix to a scheme-qualified "theme:" path. Existence is probed
// internally across the formats themes may ship (.png/.jpg/.svg), so
// callers load the returned path directly. Prefers "<prefix>-<dark|light>"
// when a file exists at that stem, otherwise the plain "<prefix>" as the
// super fallback. The resolved scheme covers explicit light/dark as well
// as OS-resolved "system". Returns the path with its file extension when a
// match is found; unqualified assets keep working unchanged.
QString assetPath(QStringView prefix) const;
// Like assetPath, but resolves only the scheme-qualified variant
// ("<prefix>-<dark|light>.<ext>") and returns an empty string when no
// variant exists — it never falls back to the plain "<prefix>" asset.
// Callers that must distinguish "no authored variant" (e.g. to keep a
// legacy runtime fallback alive) should use this instead of assetPath.
QString schemeVariantPath(QStringView prefix) const;
// Load the theme's shipped default palette, falling back to the system // Load the theme's shipped default palette, falling back to the system
// theme directory when it is absent from the resolved (user) directory. // theme directory when it is absent from the resolved (user) directory.
static PaletteConfig static PaletteConfig

View file

@ -0,0 +1,18 @@
#include "card_art_utils.h"
#include <QTransform>
#include <libcockatrice/card/printing/exact_card.h>
namespace CardArtUtils
{
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
{
if (!card.getInfo().getUiAttributes().landscapeOrientation) {
return art;
}
QTransform transform;
transform.rotate(90);
return art.transformed(transform, Qt::SmoothTransformation);
}
} // namespace CardArtUtils

View file

@ -0,0 +1,25 @@
#ifndef CARD_ART_UTILS_H
#define CARD_ART_UTILS_H
#include <QPixmap>
class ExactCard;
namespace CardArtUtils
{
/**
* @brief Rotates a card's art upright when its layout shows sideways.
*
* Sideways-layout cards (planes, sieges/battles, split cards) store their
* landscape artwork rotated 90° inside a portrait frame. Art-crop displays,
* playmat art, and the card-info picture must show such art upright before
* sampling or painting. Portrait cards are returned unchanged.
*
* @param art The card pixmap to orient.
* @param card The card describing the art orientation.
* @return @p art rotated 90° clockwise for sideways-layout cards, else @p art.
*/
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card);
} // namespace CardArtUtils
#endif // CARD_ART_UTILS_H

View file

@ -5,6 +5,7 @@
#include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/card_picture_loader/card_picture_loader.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../window_main.h" #include "../../window_main.h"
#include "card_art_utils.h"
#include <QMenu> #include <QMenu>
#include <QMouseEvent> #include <QMouseEvent>
@ -193,12 +194,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
QPixmap transformedPixmap = resizedPixmap; // Default pixmap QPixmap transformedPixmap = resizedPixmap; // Default pixmap
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
if (exactCard.getInfo().getUiAttributes().landscapeOrientation) { transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
// Rotate pixmap 90 degrees to the left
QTransform transform;
transform.rotate(90);
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
}
} }
// Handle DPI scaling // Handle DPI scaling

View file

@ -1,5 +1,6 @@
#include "abstract_analytics_panel_widget.h" #include "abstract_analytics_panel_widget.h"
#include "../../pixel_map_generator.h"
#include "deck_list_statistics_analyzer.h" #include "deck_list_statistics_analyzer.h"
#include <QPushButton> #include <QPushButton>
@ -20,7 +21,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck
// config button // config button
configureButton = new QPushButton(this); configureButton = new QPushButton(this);
configureButton->setIcon(QPixmap("theme:icons/cogwheel")); configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
bannerAndSettingsLayout->addWidget(configureButton, 0); bannerAndSettingsLayout->addWidget(configureButton, 0);

View file

@ -28,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
setFocusProxy(searchEdit); setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus); setFocusPolicy(Qt::ClickFocus);
@ -59,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
aAddCard = new QAction(QString(), this); aAddCard = new QAction(QString(), this);
aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green")); aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
auto *tbAddCard = new QToolButton(this); auto *tbAddCard = new QToolButton(this);
tbAddCard->setDefaultAction(aAddCard); tbAddCard->setDefaultAction(aAddCard);
aAddCardToSideboard = new QAction(QString(), this); aAddCardToSideboard = new QAction(QString(), this);
aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue")); aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue")));
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
auto *tbAddCardToSideboard = new QToolButton(this); auto *tbAddCardToSideboard = new QToolButton(this);
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard); tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../playmat/playmat_settings_dialog.h" #include "../playmat/playmat_settings_dialog.h"
#include "../settings_page/user_interface_settings_page.h" #include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
@ -192,25 +193,25 @@ void DeckEditorDeckDockWidget::createDeckDock()
&DeckEditorDeckDockWidget::applyActiveGroupCriteria); &DeckEditorDeckDockWidget::applyActiveGroupCriteria);
aIncrement = new QAction(QString(), this); aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment")); aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
auto *tbIncrement = new QToolButton(this); auto *tbIncrement = new QToolButton(this);
tbIncrement->setDefaultAction(aIncrement); tbIncrement->setDefaultAction(aIncrement);
aDecrement = new QAction(QString(), this); aDecrement = new QAction(QString(), this);
aDecrement->setIcon(QPixmap("theme:icons/decrement")); aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection); connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
auto *tbDecrement = new QToolButton(this); auto *tbDecrement = new QToolButton(this);
tbDecrement->setDefaultAction(aDecrement); tbDecrement->setDefaultAction(aDecrement);
aRemoveCard = new QAction(QString(), this); aRemoveCard = new QAction(QString(), this);
aRemoveCard->setIcon(QPixmap("theme:icons/remove_row")); aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard); connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
auto *tbRemoveCard = new QToolButton(this); auto *tbRemoveCard = new QToolButton(this);
tbRemoveCard->setDefaultAction(aRemoveCard); tbRemoveCard->setDefaultAction(aRemoveCard);
aSwapCard = new QAction(QString(), this); aSwapCard = new QAction(QString(), this);
aSwapCard->setIcon(QPixmap("theme:icons/swap")); aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap")));
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
auto *tbSwapCard = new QToolButton(this); auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard); tbSwapCard->setDefaultAction(aSwapCard);

View file

@ -4,6 +4,7 @@
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../../filters/filter_builder.h" #include "../../../filters/filter_builder.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "../../pixel_map_generator.h"
#include <QGridLayout> #include <QGridLayout>
#include <QMenu> #include <QMenu>
@ -42,11 +43,11 @@ void DeckEditorFilterDockWidget::createFiltersDock()
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter); connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
aClearFilterOne = new QAction(QString(), this); aClearFilterOne = new QAction(QString(), this);
aClearFilterOne->setIcon(QPixmap("theme:icons/decrement")); aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne); connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
aClearFilterAll = new QAction(QString(), this); aClearFilterAll = new QAction(QString(), this);
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch")); aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll); connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
auto *filterDelOne = new QToolButton(); auto *filterDelOne = new QToolButton();

View file

@ -1,5 +1,6 @@
#include "deck_list_history_manager_widget.h" #include "deck_list_history_manager_widget.h"
#include "../../pixel_map_generator.h"
#include "deck_state_manager.h" #include "deck_state_manager.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager, DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
@ -10,7 +11,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout = new QHBoxLayout(this); layout = new QHBoxLayout(this);
aUndo = new QAction(QString(), this); aUndo = new QAction(QString(), this);
aUndo->setIcon(QPixmap("theme:icons/arrow_undo")); aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo")));
aUndo->setShortcut(QKeySequence::Undo); aUndo->setShortcut(QKeySequence::Undo);
aUndo->setShortcutContext(Qt::ApplicationShortcut); aUndo->setShortcutContext(Qt::ApplicationShortcut);
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
@ -19,7 +20,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
undoButton->setDefaultAction(aUndo); undoButton->setDefaultAction(aUndo);
aRedo = new QAction(QString(), this); aRedo = new QAction(QString(), this);
aRedo->setIcon(QPixmap("theme:icons/arrow_redo")); aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo")));
aRedo->setShortcut(QKeySequence::Redo); aRedo->setShortcut(QKeySequence::Redo);
aRedo->setShortcutContext(Qt::ApplicationShortcut); aRedo->setShortcutContext(Qt::ApplicationShortcut);
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
@ -31,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout->addWidget(redoButton); layout->addWidget(redoButton);
historyButton = new SettingsButtonWidget(this); historyButton = new SettingsButtonWidget(this);
historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history")); historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history")));
historyLabel = new QLabel(this); historyLabel = new QLabel(this);

View file

@ -1,6 +1,7 @@
#include "dlg_connect.h" #include "dlg_connect.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
@ -21,7 +22,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this); previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(this); btnDeleteServer = new QPushButton(this);
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row")); btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30); btnDeleteServer->setFixedWidth(30);
@ -29,7 +30,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this); hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(this); btnRefreshServers = new QPushButton(this);
btnRefreshServers->setIcon(QPixmap("theme:icons/sync")); btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30); btnRefreshServers->setFixedWidth(30);
@ -99,7 +100,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
updateDisplayInfo(previousHosts->currentText()); updateDisplayInfo(previousHosts->currentText());
btnForgotPassword = new QPushButton(this); btnForgotPassword = new QPushButton(this);
btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password")); btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password")));
btnForgotPassword->setToolTip(tr("Reset Password")); btnForgotPassword->setToolTip(tr("Reset Password"));
btnForgotPassword->setFixedWidth(30); btnForgotPassword->setFixedWidth(30);
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword); connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);

View file

@ -1,5 +1,6 @@
#include "dlg_edit_tokens.h" #include "dlg_edit_tokens.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
#include <QAction> #include <QAction>
@ -90,10 +91,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
&DlgEditTokens::tokenSelectionChanged); &DlgEditTokens::tokenSelectionChanged);
QAction *aAddToken = new QAction(tr("Add token"), this); QAction *aAddToken = new QAction(tr("Add token"), this);
aAddToken->setIcon(QPixmap("theme:icons/increment")); aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken); connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
QAction *aRemoveToken = new QAction(tr("Remove token"), this); QAction *aRemoveToken = new QAction(tr("Remove token"), this);
aRemoveToken->setIcon(QPixmap("theme:icons/decrement")); aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken); connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
auto *databaseToolBar = new QToolBar; auto *databaseToolBar = new QToolBar;

View file

@ -1,6 +1,7 @@
#include "dlg_manage_sets.h" #include "dlg_manage_sets.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/card_picture_loader/card_picture_loader.h"
#include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/custom_line_edit.h"
@ -35,28 +36,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
aTop = new QAction(QString(), this); aTop = new QAction(QString(), this);
aTop->setIcon(QPixmap("theme:icons/arrow_top_green")); aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green")));
aTop->setToolTip(tr("Move selected set to the top")); aTop->setToolTip(tr("Move selected set to the top"));
aTop->setEnabled(false); aTop->setEnabled(false);
connect(aTop, &QAction::triggered, this, &WndSets::actTop); connect(aTop, &QAction::triggered, this, &WndSets::actTop);
setsEditToolBar->addAction(aTop); setsEditToolBar->addAction(aTop);
aUp = new QAction(QString(), this); aUp = new QAction(QString(), this);
aUp->setIcon(QPixmap("theme:icons/arrow_up_green")); aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green")));
aUp->setToolTip(tr("Move selected set up")); aUp->setToolTip(tr("Move selected set up"));
aUp->setEnabled(false); aUp->setEnabled(false);
connect(aUp, &QAction::triggered, this, &WndSets::actUp); connect(aUp, &QAction::triggered, this, &WndSets::actUp);
setsEditToolBar->addAction(aUp); setsEditToolBar->addAction(aUp);
aDown = new QAction(QString(), this); aDown = new QAction(QString(), this);
aDown->setIcon(QPixmap("theme:icons/arrow_down_green")); aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green")));
aDown->setToolTip(tr("Move selected set down")); aDown->setToolTip(tr("Move selected set down"));
aDown->setEnabled(false); aDown->setEnabled(false);
connect(aDown, &QAction::triggered, this, &WndSets::actDown); connect(aDown, &QAction::triggered, this, &WndSets::actDown);
setsEditToolBar->addAction(aDown); setsEditToolBar->addAction(aDown);
aBottom = new QAction(QString(), this); aBottom = new QAction(QString(), this);
aBottom->setIcon(QPixmap("theme:icons/arrow_bottom_green")); aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green")));
aBottom->setToolTip(tr("Move selected set to the bottom")); aBottom->setToolTip(tr("Move selected set to the bottom"));
aBottom->setEnabled(false); aBottom->setEnabled(false);
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom); connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
@ -66,7 +67,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
searchField = new LineEditUnfocusable; searchField = new LineEditUnfocusable;
searchField->setObjectName("searchEdit"); searchField->setObjectName("searchEdit");
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition); searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition);
searchField->setClearButtonEnabled(true); searchField->setClearButtonEnabled(true);
setFocusProxy(searchField); setFocusProxy(searchField);

View file

@ -1,6 +1,7 @@
#include "dlg_register.h" #include "dlg_register.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../server/handle_public_servers.h" #include "../server/handle_public_servers.h"
#include "../server/user/user_info_connection.h" #include "../server/user/user_info_connection.h"
@ -24,7 +25,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this); previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(this); btnDeleteServer = new QPushButton(this);
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row")); btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30); btnDeleteServer->setFixedWidth(30);
@ -32,7 +33,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this); hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(this); btnRefreshServers = new QPushButton(this);
btnRefreshServers->setIcon(QPixmap("theme:icons/sync")); btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30); btnRefreshServers->setFixedWidth(30);

View file

@ -6,6 +6,7 @@
#include "dlg_settings.h" #include "dlg_settings.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../main.h" #include "../main.h"
#include "../settings_page/appearance_settings_page.h" #include "../settings_page/appearance_settings_page.h"
#include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/deck_editor_settings_page.h"
@ -96,7 +97,7 @@ void DlgSettings::setupUi()
// Search bar // Search bar
searchEdit = new QLineEdit; searchEdit = new QLineEdit;
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
searchEdit->installEventFilter(this); searchEdit->installEventFilter(this);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
@ -132,7 +133,7 @@ void DlgSettings::setupUi()
pagesWidget->addWidget(makeScrollable(userInterfacePage)); pagesWidget->addWidget(makeScrollable(userInterfacePage));
pagesWidget->addWidget(makeScrollable(deckEditorPage)); pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage)); pagesWidget->addWidget(makeScrollable(storagePage));
pagesWidget->addWidget(messagesPage); pagesWidget->addWidget(makeScrollable(messagesPage));
pagesWidget->addWidget(soundPage); pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(shortcutsPage); pagesWidget->addWidget(shortcutsPage);
@ -477,13 +478,13 @@ void DlgSettings::closeEvent(QCloseEvent *event)
case Invalid: case Invalid:
loadErrorMessage = tr("Your card database is invalid.\n\n" loadErrorMessage = tr("Your card database is invalid.\n\n"
"Cockatrice may not function correctly with an invalid database\n\n" "Cockatrice may not function correctly with an invalid database\n\n"
"You may need to rerun oracle to update your card database.\n\n" "You may need to rerun Oracle to update your card database.\n\n"
"Would you like to change your database location setting?"); "Would you like to change your database location setting?");
break; break;
case VersionTooOld: case VersionTooOld:
loadErrorMessage = tr("Your card database version is too old.\n\n" loadErrorMessage = tr("Your card database version is too old.\n\n"
"This can cause problems loading card information or images\n\n" "This can cause problems loading card information or images\n\n"
"Usually this can be fixed by rerunning oracle to to update your card database.\n\n" "Usually this can be fixed by rerunning Oracle to to update your card database.\n\n"
"Would you like to change your database location setting?"); "Would you like to change your database location setting?");
break; break;
case NotLoaded: case NotLoaded:

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../pixel_map_generator.h"
#include "../../theme_manager.h" #include "../../theme_manager.h"
#include "../../window_main.h" #include "../../window_main.h"
#include "../cards/art_crop_attribution.h" #include "../cards/art_crop_attribution.h"
@ -20,7 +21,8 @@
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))),
overlay(themePixmap(QStringLiteral("cockatrice")))
{ {
layout = new QGridLayout(this); layout = new QGridLayout(this);
@ -56,6 +58,9 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::updateButtonsToBackgroundColor); &HomeWidget::updateButtonsToBackgroundColor);
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
// not on SettingsCache::themeChanged, so re-resolve the variant background.
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor); &HomeWidget::updateButtonsToBackgroundColor);
} }
@ -74,7 +79,7 @@ void HomeWidget::initializeBackgroundFromSource()
switch (backgroundSourceType) { switch (backgroundSourceType) {
case BackgroundSources::Theme: case BackgroundSources::Theme:
cardChangeTimer->stop(); cardChangeTimer->stop();
background = QPixmap("theme:backgrounds/home"); background = themePixmap(QStringLiteral("backgrounds/home"));
backgroundSourceDeck = DeckList(); backgroundSourceDeck = DeckList();
backgroundSourceCard->setCard(ExactCard()); backgroundSourceCard->setCard(ExactCard());
updateButtonsToBackgroundColor(); updateButtonsToBackgroundColor();

View file

@ -182,6 +182,13 @@ void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
} }
} }
void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total)
{
if (cardDatabasePage) {
cardDatabasePage->onUpdateProgress(stage, done, total);
}
}
void FirstRunWizard::finish() void FirstRunWizard::finish()
{ {
accept(); accept();

View file

@ -37,6 +37,9 @@ public slots:
/** @brief Forwarded from MainWindow once the background card database update process exits. */ /** @brief Forwarded from MainWindow once the background card database update process exits. */
void onCardDatabaseUpdateFinished(bool success); void onCardDatabaseUpdateFinished(bool success);
/** @brief Forwarded from MainWindow while the background card database update process runs. */
void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override; void changeEvent(QEvent *event) override;

View file

@ -15,6 +15,7 @@
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <climits>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
@ -179,6 +180,25 @@ void CardDatabaseSetupPage::onUpdateFinished(bool success)
} }
} }
void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total)
{
if (state != State::Running) {
return;
}
progressBar->setRange(0, total > 0 ? static_cast<int>(qMin<qint64>(total, INT_MAX)) : 0);
progressBar->setValue(static_cast<int>(qMin<qint64>(done, INT_MAX)));
if (total > 0) {
const int percent = static_cast<int>((100.0 * done) / total);
if (stage == QLatin1String("download")) {
statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent));
} else if (stage == QLatin1String("scan")) {
statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent));
} else if (stage == QLatin1String("import")) {
statusLabel->setText(tr("Importing cards (%1%)…").arg(percent));
}
}
}
QString CardDatabaseSetupPage::nextButtonText() const QString CardDatabaseSetupPage::nextButtonText() const
{ {
return state == State::NotStarted ? tr("Download") : QString(); return state == State::NotStarted ? tr("Download") : QString();

View file

@ -30,6 +30,7 @@ public:
void retranslateUi() override; void retranslateUi() override;
void onUpdateFinished(bool success); void onUpdateFinished(bool success);
void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
signals: signals:
void updateRequested(); void updateRequested();

View file

@ -2,6 +2,7 @@
#include "../../card_picture_loader/card_picture_loader.h" #include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/art_crop_attribution.h" #include "../cards/art_crop_attribution.h"
#include "../cards/card_art_utils.h"
#include "../utility/completer_utils.h" #include "../utility/completer_utils.h"
#include "card_database_display_model.h" #include "card_database_display_model.h"
#include "card_database_model.h" #include "card_database_model.h"
@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
return; return;
} }
currentPixmap = fullRes; currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
preview->setParams(currentParams); preview->setParams(currentParams);
preview->setAttribution(buildArtAttribution(card)); preview->setAttribution(buildArtAttribution(card));

View file

@ -1,5 +1,7 @@
#include "settings_button_widget.h" #include "settings_button_widget.h"
#include "../../pixel_map_generator.h"
#include <QApplication> #include <QApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
@ -8,7 +10,7 @@
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr)) : QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr))
{ {
button->setIcon(QPixmap("theme:icons/cogwheel")); button->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
button->setCheckable(true); button->setCheckable(true);
button->setFixedSize(32, 32); button->setFixedSize(32, 32);
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);

View file

@ -142,8 +142,10 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode)
} }
// backwards skip => always skip tap animation // backwards skip => always skip tap animation
// backwards skip => always skip damage animation (battlefield shimmer / life counter flash)
if (playbackMode == BACKWARD_SKIP) { if (playbackMode == BACKWARD_SKIP) {
options |= SKIP_TAP_ANIMATION; options |= SKIP_TAP_ANIMATION;
options |= SKIP_DAMAGE_ANIMATION;
} }
emit eventReplayed(replay->event_list(currentEvent), options); emit eventReplayed(replay->event_list(currentEvent), options);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
#include "replay_manager.h" #include "replay_manager.h"
#include "replay_quick_settings_widget.h" #include "replay_quick_settings_widget.h"
@ -50,15 +51,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
replayPlayButton = new QToolButton; replayPlayButton = new QToolButton;
replayPlayButton->setIconSize(QSize(32, 32)); replayPlayButton->setIconSize(QSize(32, 32));
QIcon playButtonIcon = QIcon(); QIcon playButtonIcon = QIcon();
playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off); playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off);
playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On); playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On);
replayPlayButton->setIcon(playButtonIcon); replayPlayButton->setIcon(playButtonIcon);
replayPlayButton->setCheckable(true); replayPlayButton->setCheckable(true);
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled); connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
replayFastForwardButton = new QToolButton; replayFastForwardButton = new QToolButton;
replayFastForwardButton->setIconSize(QSize(32, 32)); replayFastForwardButton->setIconSize(QSize(32, 32));
replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward")); replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward")));
replayFastForwardButton->setCheckable(true); replayFastForwardButton->setCheckable(true);
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor); connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);

View file

@ -1,5 +1,6 @@
#include "game_selector.h" #include "game_selector.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/dialogs/dlg_filter_games.h" #include "../interface/widgets/dialogs/dlg_filter_games.h"
#include "../interface/widgets/tabs/tab_account.h" #include "../interface/widgets/tabs/tab_account.h"
@ -95,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client,
} }
filterButton = new QPushButton; filterButton = new QPushButton;
filterButton->setIcon(QPixmap("theme:icons/search")); filterButton->setIcon(themePixmap(QStringLiteral("icons/search")));
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter); connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
clearFilterButton = new QPushButton; clearFilterButton = new QPushButton;
clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch")); clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
clearFilterButton->setEnabled(!filtersSetToDefault); clearFilterButton->setEnabled(!filtersSetToDefault);
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);

View file

@ -1,5 +1,7 @@
#include "remote_replay_list_tree_widget.h" #include "remote_replay_list_tree_widget.h"
#include "../../../pixel_map_generator.h"
#include <QFileIconProvider> #include <QFileIconProvider>
#include <QHeaderView> #include <QHeaderView>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
@ -37,7 +39,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client,
QFileIconProvider fip; QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder); dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File); fileIcon = fip.icon(QFileIconProvider::File);
lockIcon = QPixmap("theme:icons/lock"); lockIcon = themePixmap(QStringLiteral("icons/lock"));
} }
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel() RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()

View file

@ -1,6 +1,7 @@
#include "user_card_art_provider.h" #include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h" #include "../../../card_picture_loader/card_picture_loader.h"
#include "../../cards/card_art_utils.h"
#include <QPointer> #include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
processQueue(); processQueue();
} }
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card)
{ {
const QSize sz = fullRes.size(); QPixmap source = fullRes;
// Sideways-layout cards (plane, siege/battle, split) store their landscape
// artwork rotated 90° inside a portrait frame. Rotate it upright first so
// the crop below lands on the horizontal art, mirroring the way
// CardInfoPictureWidget displays these cards.
const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation;
source = CardArtUtils::rotateSidewaysLayoutArt(source, card);
const QSize sz = source.size();
const int marginX = sz.width() * 0.07; const int marginX = sz.width() * 0.07;
const int topMargin = sz.height() * 0.11; const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
const int bottomMargin = sz.height() * 0.45; const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45;
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
return fullRes.copy(foilRect.intersected(fullRes.rect())); return source.copy(artRect.intersected(source.rect()));
} }
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
// Synchronous hit (already loaded/on disk) // Synchronous hit (already loaded/on disk)
if (!fullRes.isNull()) { if (!fullRes.isNull()) {
insertIntoCache(key, cropCardArt(fullRes)); insertIntoCache(key, cropCardArt(fullRes, card));
pending.remove(key); pending.remove(key);
emit cardArtUpdated(userName); emit cardArtUpdated(userName);
@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) { if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes)); self->insertIntoCache(key, self->cropCardArt(fullRes, card));
} }
self->pending.remove(key); self->pending.remove(key);

View file

@ -6,6 +6,7 @@
#include <QPixmap> #include <QPixmap>
#include <QQueue> #include <QQueue>
#include <QSet> #include <QSet>
#include <libcockatrice/card/printing/exact_card.h>
class UserCardArtProvider : public QObject class UserCardArtProvider : public QObject
{ {
@ -16,7 +17,7 @@ public:
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap<QString, QPixmap> &cache() const; const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes); static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
signals: signals:
void cardArtUpdated(const QString &userName); void cardArtUpdated(const QString &userName);

View file

@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
return; return;
} }
currentPixmap = UserCardArtProvider::cropCardArt(fullRes); currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
preview->setParams(currentParams); preview->setParams(currentParams);

View file

@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromMod = new QAction(QString(), this); aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this);
aPromoteToDeveloper = new QAction(QString(), this);
aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this); aInvestigateUser = new QAction(QString(), this);
@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
aDemoteFromMod->setText(tr("Dem&ote user from moderator")); aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge")); aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge")); aDemoteFromJudge->setText(tr("Demote user from judge"));
aPromoteToDeveloper->setText(tr("Promote user to &developer"));
aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
aGetAdminNotes->setText(tr("View admin notes")); aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user")); aInvestigateUser->setText(tr("Investigate user"));
} }
@ -268,7 +272,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext); const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) { if (resp.response_code() == Response::RespOk) {
if (cmd.should_be_mod() || cmd.should_be_judge()) { if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"), QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
tr("Successfully promoted user.")); tr("Successfully promoted user."));
} else { } else {
@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
} }
} else { } else {
if (cmd.should_be_mod() || cmd.should_be_judge()) { if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user.")); QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
} else { } else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user.")); QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
@ -437,9 +441,18 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge); menu->addAction(aPromoteToJudge);
} }
if (userLevel.testFlag(ServerInfo_User::IsDeveloper) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromDeveloper);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToDeveloper);
}
} }
aDetails->setEnabled(true); aDetails->setEnabled(true);
aChat->setEnabled(anotherUser && online); aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
aShowGames->setEnabled(online); aShowGames->setEnabled(online);
aReport->setEnabled(anotherUser); aReport->setEnabled(anotherUser);
aAddToBuddyList->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser);
@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aInvestigateUser->setEnabled(anotherUser); aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser);
aPromoteToJudge->setEnabled(anotherUser);
aDemoteFromJudge->setEnabled(anotherUser);
aPromoteToDeveloper->setEnabled(anotherUser);
aDemoteFromDeveloper->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos); QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) { if (actionClicked == nullptr) {
@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdjustMod(userName, actionClicked == aPromoteToMod); execAdjustMod(userName, actionClicked == aPromoteToMod);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) { } else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
execAdjustJudge(userName, actionClicked == aPromoteToJudge); execAdjustJudge(userName, actionClicked == aPromoteToJudge);
} else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
} else if (actionClicked == aBanHistory) { } else if (actionClicked == aBanHistory) {
execBanHistory(userName); execBanHistory(userName);
} else if (actionClicked == aWarnUser) { } else if (actionClicked == aWarnUser) {
@ -606,7 +625,15 @@ void UserContextMenu::execAddToIgnore(const QString &userName)
Command_AddToList cmd; Command_AddToList cmd;
cmd.set_list("ignore"); cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString()); cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd)); PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this,
[this, userName](const Response &response, const CommandContainer &, const QVariant &) {
if (response.response_code() == Response::RespOk) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Ignore list"),
tr("%1 has been added to your ignore list.").arg(userName));
}
});
client->sendCommand(pend);
} }
void UserContextMenu::execRemoveFromIgnore(const QString &userName) void UserContextMenu::execRemoveFromIgnore(const QString &userName)
@ -698,4 +725,14 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
PendingCommand *pend = client->prepareAdminCommand(cmd); PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }
void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper)
{
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_developer(shouldBeDeveloper);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
}

View file

@ -45,6 +45,7 @@ private:
QAction *aBan, *aBanHistory; QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aPromoteToJudge, *aDemoteFromJudge;
QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
QAction *aWarnUser, *aWarnHistory; QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes; QAction *aGetAdminNotes;
std::function<QList<GameInviteOption>()> gameInviteLinkProvider; std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
@ -123,6 +124,7 @@ public:
void execInvestigateUser(const QString &userName); void execInvestigateUser(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge); void execAdjustJudge(const QString &userName, bool shouldBeJudge);
void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
private: private:
void execInvite(const QString &userName, const GameInviteOption &option); void execInvite(const QString &userName, const GameInviteOption &option);

View file

@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
QString userLevelText; QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator"); userLevelText = tr("Administrator");
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
userLevelText = tr("Developer");
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
userLevelText = tr("Moderator"); userLevelText = tr("Moderator");
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { } else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {

View file

@ -245,6 +245,9 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11); return QColor(245, 158, 11);
} }
if (level.testFlag(ServerInfo_User::IsDeveloper)) {
return QColor(185, 28, 28);
}
if (level.testFlag(ServerInfo_User::IsModerator)) { if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246); return QColor(59, 130, 246);
} }
@ -300,6 +303,8 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
} badge; } badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)}; badge = {"ADMIN", QColor(245, 158, 11)};
} else if (level.testFlag(ServerInfo_User::IsDeveloper)) {
badge = {"DEV", QColor(185, 28, 28)};
} else if (level.testFlag(ServerInfo_User::IsModerator)) { } else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)}; badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) { } else if (level.testFlag(ServerInfo_User::IsJudge)) {

View file

@ -49,6 +49,8 @@ QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool onl
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11); accentColor = QColor(245, 158, 11);
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
accentColor = QColor(185, 28, 28);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246); accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@ -299,6 +301,8 @@ QList<UserListPainter::Badge> UserListPainter::buildBadges(const UserLevelFlags
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)}; badges << Badge{"ADMIN", QColor(245, 158, 11)};
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
badges << Badge{"DEV", QColor(185, 28, 28)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)}; badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@ -385,9 +389,9 @@ void UserListPainter::paint(QPainter *painter,
const QString userName = QString::fromStdString(userInfo.name()); const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel()); const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online); const QColor accentColor = getAccentColor(userLevel, online);
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) || const bool hasRole =
userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) ||
userLevel.testFlag(ServerInfo_User::IsJudge); userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect); const int cardRight = getCardRight(option, rect);

View file

@ -234,9 +234,11 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt()); const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
// Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator // Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer
// or a moderator
static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = { static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = {
ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator}; ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper,
ServerInfo_User_UserLevelFlag_IsModerator};
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) { for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) && if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) { lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {

View file

@ -1,6 +1,7 @@
#include "deck_editor_settings_page.h" #include "deck_editor_settings_page.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "update/card_spoiler/spoiler_background_updater.h" #include "update/card_spoiler/spoiler_background_updater.h"
#include <QFileDialog> #include <QFileDialog>
@ -53,15 +54,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
urlList->addItems(SettingsCache::instance().downloads().getAllURLs()); urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
aAdd = new QAction(this); aAdd = new QAction(this);
aAdd->setIcon(QPixmap("theme:icons/increment")); aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL); connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
aEdit = new QAction(this); aEdit = new QAction(this);
aEdit->setIcon(QPixmap("theme:icons/pencil")); aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL); connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
aRemove = new QAction(this); aRemove = new QAction(this);
aRemove->setIcon(QPixmap("theme:icons/decrement")); aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL); connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
auto *urlToolBar = new QToolBar; auto *urlToolBar = new QToolBar;

View file

@ -1,6 +1,7 @@
#include "messages_settings_page.h" #include "messages_settings_page.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
#include <QGridLayout> #include <QGridLayout>
@ -59,6 +60,10 @@ MessagesSettingsPage::MessagesSettingsPage()
connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setRoomHistory); &ChatSettings::setRoomHistory);
ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages());
connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setIgnoreAllPrivateMessages);
customAlertString = new QLineEdit(); customAlertString = new QLineEdit();
customAlertString->setText(SettingsCache::instance().chat().getHighlightWords()); customAlertString->setText(SettingsCache::instance().chat().getHighlightWords());
connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(), connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(),
@ -76,6 +81,7 @@ MessagesSettingsPage::MessagesSettingsPage()
chatGrid->addWidget(&messagePopups, 5, 0); chatGrid->addWidget(&messagePopups, 5, 0);
chatGrid->addWidget(&mentionPopups, 6, 0); chatGrid->addWidget(&mentionPopups, 6, 0);
chatGrid->addWidget(&roomHistory, 7, 0); chatGrid->addWidget(&roomHistory, 7, 0);
chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0);
chatGroupBox = new QGroupBox; chatGroupBox = new QGroupBox;
chatGroupBox->setLayout(chatGrid); chatGroupBox->setLayout(chatGrid);
@ -102,15 +108,15 @@ MessagesSettingsPage::MessagesSettingsPage()
} }
aAdd = new QAction(this); aAdd = new QAction(this);
aAdd->setIcon(QPixmap("theme:icons/increment")); aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd); connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
aEdit = new QAction(this); aEdit = new QAction(this);
aEdit->setIcon(QPixmap("theme:icons/pencil")); aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit); connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
aRemove = new QAction(this); aRemove = new QAction(this);
aRemove->setIcon(QPixmap("theme:icons/decrement")); aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove); connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
auto *messageToolBar = new QToolBar; auto *messageToolBar = new QToolBar;
@ -246,6 +252,7 @@ void MessagesSettingsPage::retranslateUi()
messagePopups.setText(tr("Enable desktop notifications for private messages")); messagePopups.setText(tr("Enable desktop notifications for private messages"));
mentionPopups.setText(tr("Enable desktop notification for mentions")); mentionPopups.setText(tr("Enable desktop notification for mentions"));
roomHistory.setText(tr("Enable room message history on join")); roomHistory.setText(tr("Enable room message history on join"));
ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages"));
hexLabel.setText(tr("(Color is hexadecimal)")); hexLabel.setText(tr("(Color is hexadecimal)"));
hexHighlightLabel.setText(tr("(Color is hexadecimal)")); hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));

View file

@ -40,6 +40,7 @@ private:
QCheckBox messagePopups; QCheckBox messagePopups;
QCheckBox mentionPopups; QCheckBox mentionPopups;
QCheckBox roomHistory; QCheckBox roomHistory;
QCheckBox ignoreAllPrivateMessagesCheckBox;
QGroupBox *chatGroupBox; QGroupBox *chatGroupBox;
QGroupBox *highlightGroupBox; QGroupBox *highlightGroupBox;
QGroupBox *messageGroupBox; QGroupBox *messageGroupBox;

View file

@ -3,6 +3,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcut_treeview.h" #include "../../../client/settings/shortcut_treeview.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/custom_line_edit.h"
#include "../interface/widgets/utility/sequence_edit.h" #include "../interface/widgets/utility/sequence_edit.h"
@ -47,8 +48,8 @@ ShortcutSettingsPage::ShortcutSettingsPage()
btnResetAll = new QPushButton(this); btnResetAll = new QPushButton(this);
btnClearAll = new QPushButton(this); btnClearAll = new QPushButton(this);
btnResetAll->setIcon(QPixmap("theme:icons/update")); btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update")));
btnClearAll->setIcon(QPixmap("theme:icons/clearsearch")); btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
// layout // layout
auto *_editLayout = new QGridLayout; auto *_editLayout = new QGridLayout;

View file

@ -1,6 +1,7 @@
#include "tab_archidekt.h" #include "tab_archidekt.h"
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../../pixel_map_generator.h"
#include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../../cards/additional_info/mana_symbol_widget.h"
#include "../../../utility/completer_utils.h" #include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h" #include "../../tab_supervisor.h"
@ -213,7 +214,7 @@ void TabArchidekt::setupFilterWidgets()
// Format filter (collapsible) // Format filter (collapsible)
formatButton = new SettingsButtonWidget(secondaryToolbar); formatButton = new SettingsButtonWidget(secondaryToolbar);
formatButton->setButtonText(tr("Formats")); formatButton->setButtonText(tr("Formats"));
formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced")); formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
QWidget *formatContainer = new QWidget(secondaryToolbar); QWidget *formatContainer = new QWidget(secondaryToolbar);
QGridLayout *formatLayout = new QGridLayout(formatContainer); QGridLayout *formatLayout = new QGridLayout(formatContainer);

View file

@ -1,6 +1,7 @@
#include "commander_bracket_widget.h" #include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../../pixel_map_generator.h"
#include "commander_bracket_service.h" #include "commander_bracket_service.h"
#include <QComboBox> #include <QComboBox>
@ -30,7 +31,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent
bracketInfoButton->setEnabled(false); bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this); bracketRefreshButton = new QToolButton(this);
bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
bracketRefreshButton->setAutoRaise(true); bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate);

View file

@ -137,6 +137,11 @@ void TabAccount::retranslateUi()
buddyList->retranslateUi(); buddyList->retranslateUi();
ignoreList->retranslateUi(); ignoreList->retranslateUi();
userInfoBox->retranslateUi(); userInfoBox->retranslateUi();
buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the "
"server, and they can be invited to buddy-only games."));
ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private "
"messages or join your games."));
} }
void TabAccount::processListUsersResponse(const Response &response) void TabAccount::processListUsersResponse(const Response &response)

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
@ -105,19 +106,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Left side actions // Left side actions
aOpenLocalDeck = new QAction(this); aOpenLocalDeck = new QAction(this);
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil")); aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck); connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck);
aRenameLocal = new QAction(this); aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/rename")); aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal); connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this); aUpload = new QAction(this);
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green")); aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload); connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload);
aNewLocalFolder = new QAction(this); aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder); connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this); aDeleteLocalDeck = new QAction(this);
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row")); aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck); connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
aOpenDecksFolder = new QAction(this); aOpenDecksFolder = new QAction(this);
@ -126,16 +127,16 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Right side actions // Right side actions
aOpenRemoteDeck = new QAction(this); aOpenRemoteDeck = new QAction(this);
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil")); aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck); connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck);
aDownload = new QAction(this); aDownload = new QAction(this);
aDownload->setIcon(QPixmap("theme:icons/arrow_left_green")); aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload); connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload);
aNewFolder = new QAction(this); aNewFolder = new QAction(this);
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder); connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
aDeleteRemoteDeck = new QAction(this); aDeleteRemoteDeck = new QAction(this);
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row")); aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
// Add actions to toolbars // Add actions to toolbars

View file

@ -0,0 +1,261 @@
/**
* @file tab_developer.cpp
* @ingroup ServerTabs
*/
//! \todo Document this file.
#include "tab_developer.h"
#include <QCheckBox>
#include <QDateTime>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
#include <libcockatrice/protocol/pb/response_get_server_stats.pb.h>
#include <libcockatrice/protocol/pending_command.h>
static constexpr int DEFAULT_AUTO_REFRESH_INTERVAL_SECS = 30;
TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client)
: Tab(_tabSupervisor), client(_client)
{
statsTable = new QTableWidget(0, 2);
statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
statsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
statsTable->setSelectionMode(QAbstractItemView::SingleSelection);
statsTable->verticalHeader()->setVisible(false);
statsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
statsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
statsTable->horizontalHeader()->setStretchLastSection(true);
commandTable = new QTableWidget(0, 4);
commandTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
commandTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
commandTable->setSelectionBehavior(QAbstractItemView::SelectRows);
commandTable->setSelectionMode(QAbstractItemView::SingleSelection);
commandTable->verticalHeader()->setVisible(false);
commandTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive);
statusLabel = new QLabel;
autoRefreshCheckBox = new QCheckBox;
autoRefreshCheckBox->setChecked(false);
refreshIntervalSpinBox = new QSpinBox;
refreshIntervalSpinBox->setRange(5, 3600);
refreshIntervalSpinBox->setValue(DEFAULT_AUTO_REFRESH_INTERVAL_SECS);
refreshIntervalSpinBox->setEnabled(false);
autoRefreshTimer = new QTimer(this);
connect(autoRefreshTimer, &QTimer::timeout, this, &TabDeveloper::refreshClicked);
connect(autoRefreshCheckBox, &QCheckBox::toggled, this, &TabDeveloper::autoRefreshToggled);
connect(refreshIntervalSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
&TabDeveloper::refreshIntervalChanged);
refreshButton = new QPushButton;
refreshButton->setAutoDefault(true);
connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked);
auto *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft);
buttonLayout->addWidget(autoRefreshCheckBox, 0, Qt::AlignRight);
buttonLayout->addWidget(refreshIntervalSpinBox, 0, Qt::AlignRight);
buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight);
auto *tableLayout = new QHBoxLayout;
tableLayout->addWidget(statsTable, 1);
tableLayout->addWidget(commandTable, 2);
auto *mainLayout = new QVBoxLayout;
mainLayout->addLayout(tableLayout, 1);
mainLayout->addLayout(buttonLayout);
auto *central = new QWidget;
central->setLayout(mainLayout);
setCentralWidget(central);
retranslateUi();
}
void TabDeveloper::retranslateUi()
{
autoRefreshCheckBox->setText(tr("Auto-refresh"));
autoRefreshCheckBox->setToolTip(tr("Automatically request fresh server statistics at a fixed interval."));
refreshIntervalSpinBox->setSuffix(tr(" s"));
refreshIntervalSpinBox->setToolTip(tr("Seconds between automatic refreshes."));
refreshButton->setText(tr("Refresh server stats"));
statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";"));
commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";"));
if (statsTable->rowCount() == 0) {
statusLabel->clear();
}
}
QString TabDeveloper::formatBytes(quint64 bytes)
{
const quint64 kib = 1024;
const quint64 mib = 1024 * kib;
const quint64 gib = 1024 * mib;
if (bytes >= gib) {
return tr("%1 GiB").arg(QString::number(bytes / static_cast<double>(gib), 'f', 2));
}
if (bytes >= mib) {
return tr("%1 MiB").arg(QString::number(bytes / static_cast<double>(mib), 'f', 2));
}
if (bytes >= kib) {
return tr("%1 KiB").arg(QString::number(bytes / static_cast<double>(kib), 'f', 2));
}
return tr("%1 bytes").arg(bytes);
}
QString TabDeveloper::formatDurationMs(qint64 ms)
{
if (ms >= 1000) {
return tr("%1 s").arg(QString::number(ms / 1000.0, 'f', 2));
}
return tr("%1 ms").arg(ms);
}
void TabDeveloper::appendStatRow(const QString &name, const QString &value)
{
const int row = statsTable->rowCount();
statsTable->insertRow(row);
statsTable->setItem(row, 0, new QTableWidgetItem(name));
statsTable->setItem(row, 1, new QTableWidgetItem(value));
}
void TabDeveloper::appendSeparatorRow(const QString &sectionTitle)
{
const int row = statsTable->rowCount();
statsTable->insertRow(row);
auto *labelItem = new QTableWidgetItem(sectionTitle);
auto font = labelItem->font();
font.setBold(true);
labelItem->setFont(font);
labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable);
statsTable->setItem(row, 0, labelItem);
statsTable->setItem(row, 1, new QTableWidgetItem(QString()));
}
void TabDeveloper::refreshClicked()
{
if (requestPending) {
return;
}
requestPending = true;
Command_GetServerStats cmd;
PendingCommand *pend = client->prepareDeveloperCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse);
client->sendCommand(pend);
}
void TabDeveloper::autoRefreshToggled(bool checked)
{
refreshIntervalSpinBox->setEnabled(checked);
if (checked) {
refreshIntervalChanged();
refreshClicked();
} else {
autoRefreshTimer->stop();
}
}
void TabDeveloper::refreshIntervalChanged()
{
if (autoRefreshCheckBox->isChecked()) {
autoRefreshTimer->start(refreshIntervalSpinBox->value() * 1000);
}
}
void TabDeveloper::serverStatsResponse(const Response &resp)
{
requestPending = false;
if (resp.response_code() != Response::RespOk) {
statusLabel->setText(tr("No server statistics available yet."));
return;
}
const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext);
statsTable->setRowCount(0);
// Overview section
appendStatRow(tr("Registered users online"), QString::number(response.users_count()));
appendStatRow(tr("Moderators online"), QString::number(response.mods_count()));
appendStatRow(tr("Games running"), QString::number(response.games_count()));
appendStatRow(tr("Traffic sent (last tick)"), formatBytes(response.tx_bytes()));
appendStatRow(tr("Traffic received (last tick)"), formatBytes(response.rx_bytes()));
const qint64 uptime = static_cast<qint64>(response.uptime_secs());
const int days = static_cast<int>(uptime / 86400);
const int hours = static_cast<int>((uptime % 86400) / 3600);
const int minutes = static_cast<int>((uptime % 3600) / 60);
appendStatRow(tr("Server uptime"), days > 0 ? tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes)
: tr("%1h %2m").arg(hours).arg(minutes));
const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(response.timest()));
appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm"));
// Live metrics section
appendSeparatorRow(tr("Live Metrics"));
appendStatRow(tr("Cards in live games"), QString::number(response.cards_in_games()));
appendStatRow(tr("Total commands processed"), QString::number(response.total_commands()));
if (response.total_commands() > 0) {
const double avgMs = static_cast<double>(response.total_command_time_ms()) / response.total_commands();
appendStatRow(tr("Avg command time"), QString::number(avgMs, 'f', 2) + " ms");
}
appendStatRow(tr("Active command types"), QString::number(response.active_command_types()));
appendStatRow(tr("Event loop stalls"), QString::number(response.eventloop_stalls_total()));
appendStatRow(tr("Last stall overshoot"), formatDurationMs(response.eventloop_last_stall_ms()));
appendStatRow(tr("Worst stall overshoot"), formatDurationMs(response.eventloop_max_stall_ms()));
if (response.game_start_count() > 0) {
appendStatRow(tr("Game starts"), QString::number(response.game_start_count()));
const double avgStartMs = static_cast<double>(response.game_start_total_ms()) / response.game_start_count();
appendStatRow(tr("Avg game start time"), QString::number(avgStartMs, 'f', 1) + " ms");
}
// Per-command breakdown table
QList<CommandStats> sortedStats(response.command_stats().begin(), response.command_stats().end());
std::sort(sortedStats.begin(), sortedStats.end(),
[](const auto &a, const auto &b) { return a.total_ms() > b.total_ms(); });
commandTable->setRowCount(0);
for (const auto &cs : sortedStats) {
const int row = commandTable->rowCount();
commandTable->insertRow(row);
commandTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(cs.command_name())));
auto *countItem = new QTableWidgetItem(QString::number(cs.count()));
countItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 1, countItem);
auto *totalItem = new QTableWidgetItem(QString::number(cs.total_ms()));
totalItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 2, totalItem);
const double avg = cs.count() > 0 ? static_cast<double>(cs.total_ms()) / cs.count() : 0.0;
auto *avgItem = new QTableWidgetItem(QString::number(avg, 'f', 2));
avgItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 3, avgItem);
}
commandTable->resizeColumnsToContents();
statsTable->resizeColumnsToContents();
commandTable->resizeColumnsToContents();
statusLabel->setText(tr("Updated %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm")));
}

View file

@ -0,0 +1,55 @@
/**
* @file tab_developer.h
* @ingroup ServerTabs
*/
//! \todo Document this file.
#ifndef TAB_DEVELOPER_H
#define TAB_DEVELOPER_H
#include "tab.h"
class AbstractClient;
class QCheckBox;
class QLabel;
class QPushButton;
class QSpinBox;
class QTableWidget;
class QTimer;
class Response;
class TabDeveloper : public Tab
{
Q_OBJECT
private:
AbstractClient *client;
QTableWidget *statsTable;
QTableWidget *commandTable;
QPushButton *refreshButton;
QLabel *statusLabel;
QCheckBox *autoRefreshCheckBox;
QSpinBox *refreshIntervalSpinBox;
QTimer *autoRefreshTimer;
bool requestPending = false;
void appendStatRow(const QString &name, const QString &value);
void appendSeparatorRow(const QString &sectionTitle);
static QString formatBytes(quint64 bytes);
static QString formatDurationMs(qint64 ms);
private slots:
void refreshClicked();
void serverStatsResponse(const Response &resp);
void autoRefreshToggled(bool checked);
void refreshIntervalChanged();
public:
explicit TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Developer");
}
};
#endif

View file

@ -266,6 +266,10 @@ void TabGame::resetChatAndPhase()
// reset phase markers // reset phase markers
game->getGameState()->setCurrentPhase(-1); game->getGameState()->setCurrentPhase(-1);
// reset spectator state so the replay can rebuild it from the start
game->getPlayerManager()->clearSpectators();
playerListWidget->clearSpectators();
} }
void TabGame::emitUserEvent() void TabGame::emitUserEvent()

View file

@ -19,7 +19,8 @@
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
{ {
roomTable = new QTableWidget(); roomTable = new QTableWidget();
roomTable->setColumnCount(6); roomTable->setColumnCount(6);
@ -80,7 +81,9 @@ void TabLog::getClicked()
if (!mainRoom->isChecked() && !gameRoom->isChecked() && !privateChat->isChecked()) { if (!mainRoom->isChecked() && !gameRoom->isChecked() && !privateChat->isChecked()) {
mainRoom->setChecked(true); mainRoom->setChecked(true);
gameRoom->setChecked(true); gameRoom->setChecked(true);
privateChat->setChecked(true); if (!canUseDeveloperCommands) {
privateChat->setChecked(true);
}
} }
if (maximumResults->value() == 0) { if (maximumResults->value() == 0) {
@ -117,7 +120,15 @@ void TabLog::getClicked()
}; };
cmd.set_date_range(dateRange); cmd.set_date_range(dateRange);
cmd.set_maximum_results(maximumResults->value()); cmd.set_maximum_results(maximumResults->value());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
PendingCommand *pend;
if (canUseDeveloperCommands) {
// Developers query logs through the developer command family.
pend = client->prepareDeveloperCommand(cmd);
} else {
pend = client->prepareModeratorCommand(cmd);
}
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }
@ -171,6 +182,14 @@ void TabLog::createDock()
mainRoom = new QCheckBox(tr("Main Room")); mainRoom = new QCheckBox(tr("Main Room"));
gameRoom = new QCheckBox(tr("Game Room")); gameRoom = new QCheckBox(tr("Game Room"));
privateChat = new QCheckBox(tr("Private Chat")); privateChat = new QCheckBox(tr("Private Chat"));
if (canUseDeveloperCommands) {
// Developers cannot query private conversations.
privateChat->setVisible(false);
// The developer family ignores the IP filter server-side, so showing
// the field would silently unfilter the result by it. Hide it.
labelFindIPAddress->setVisible(false);
findIPAddress->setVisible(false);
}
pastDays = new QRadioButton(tr("Past X Days: ")); pastDays = new QRadioButton(tr("Past X Days: "));
today = new QRadioButton(tr("Today")); today = new QRadioButton(tr("Today"));

View file

@ -33,6 +33,7 @@ class TabLog : public Tab
Q_OBJECT Q_OBJECT
private: private:
AbstractClient *client; AbstractClient *client;
bool canUseDeveloperCommands;
QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum, QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum,
*labelDescription; *labelDescription;
LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage; LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage;
@ -58,7 +59,7 @@ private slots:
void restartLayout(); void restartLayout();
public: public:
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client); TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands = false);
~TabLog() override; ~TabLog() override;
void retranslateUi() override; void retranslateUi() override;
[[nodiscard]] QString getTabText() const override [[nodiscard]] QString getTabText() const override

View file

@ -98,6 +98,12 @@ void TabMessage::closeEvent(QCloseEvent *event)
void TabMessage::sendPrivateMessage(const QString &text) void TabMessage::sendPrivateMessage(const QString &text)
{ {
if (tabSupervisor->getUserListManager()->isUserIgnored(getUserName())) {
chatView->appendMessage(tr("You have ignored %1; your messages are not delivered.")
.arg(QString::fromStdString(otherUserInfo->name())));
return;
}
Command_Message cmd; Command_Message cmd;
cmd.set_user_name(otherUserInfo->name()); cmd.set_user_name(otherUserInfo->name());
cmd.set_message(text.toStdString()); cmd.set_message(text.toStdString());

View file

@ -381,6 +381,9 @@ void TabModeration::moderatorLoginsResponse(const Response &response)
if (login.user_level() & ServerInfo_User::IsAdmin) { if (login.user_level() & ServerInfo_User::IsAdmin) {
levels << tr("Admin"); levels << tr("Admin");
} }
if (login.user_level() & ServerInfo_User::IsDeveloper) {
levels << tr("Developer");
}
if (login.user_level() & ServerInfo_User::IsModerator) { if (login.user_level() & ServerInfo_User::IsModerator) {
levels << tr("Moderator"); levels << tr("Moderator");
} }

View file

@ -1,6 +1,7 @@
#include "tab_replays.h" #include "tab_replays.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h" #include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h"
#include "tab_game.h" #include "tab_game.h"
@ -102,17 +103,17 @@ QGroupBox *TabReplays::createLeftLayout()
// Left side actions // Left side actions
aOpenLocalReplay = new QAction(this); aOpenLocalReplay = new QAction(this);
aOpenLocalReplay->setIcon(QPixmap("theme:icons/view")); aOpenLocalReplay->setIcon(themePixmap(QStringLiteral("icons/view")));
connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay); connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay);
connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay); connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay);
aRenameLocal = new QAction(this); aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/rename")); aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal); connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
aNewLocalFolder = new QAction(this); aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder); connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
aDeleteLocalReplay = new QAction(this); aDeleteLocalReplay = new QAction(this);
aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row")); aDeleteLocalReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay); connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay);
aOpenReplaysFolder = new QAction(this); aOpenReplaysFolder = new QAction(this);
@ -164,24 +165,24 @@ QGroupBox *TabReplays::createRightLayout()
// Right side actions // Right side actions
aOpenRemoteReplay = new QAction(this); aOpenRemoteReplay = new QAction(this);
aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view")); aOpenRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/view")));
connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay); connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay);
connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay); connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay);
aDownload = new QAction(this); aDownload = new QAction(this);
aDownload->setIcon(QPixmap("theme:icons/arrow_left_green")); aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload); connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload);
aKeep = new QAction(this); aKeep = new QAction(this);
aKeep->setIcon(QPixmap("theme:icons/lock")); aKeep->setIcon(themePixmap(QStringLiteral("icons/lock")));
connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay); connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay);
aDeleteRemoteReplay = new QAction(this); aDeleteRemoteReplay = new QAction(this);
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row")); aDeleteRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay); connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay);
aGetReplayCode = new QAction(this); aGetReplayCode = new QAction(this);
aGetReplayCode->setIcon(QPixmap("theme:icons/share")); aGetReplayCode->setIcon(themePixmap(QStringLiteral("icons/share")));
connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode); connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode);
aSubmitReplayCode = new QAction(this); aSubmitReplayCode = new QAction(this);
aSubmitReplayCode->setIcon(QPixmap("theme:icons/search")); aSubmitReplayCode->setIcon(themePixmap(QStringLiteral("icons/search")));
connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode); connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode);
// Add actions to toolbars // Add actions to toolbars

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/dialogs/dlg_settings.h" #include "../interface/widgets/dialogs/dlg_settings.h"
#include "../interface/widgets/server/chat_view/chat_view.h" #include "../interface/widgets/server/chat_view/chat_view.h"
#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/game_link.h"
@ -98,7 +99,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings); connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings);
auto *chatSettingsButton = new QToolButton; auto *chatSettingsButton = new QToolButton;
chatSettingsButton->setIcon(QPixmap("theme:icons/settings")); chatSettingsButton->setIcon(themePixmap(QStringLiteral("icons/settings")));
chatSettingsButton->setMenu(chatSettingsMenu); chatSettingsButton->setMenu(chatSettingsMenu);
chatSettingsButton->setPopupMode(QToolButton::InstantPopup); chatSettingsButton->setPopupMode(QToolButton::InstantPopup);

View file

@ -15,6 +15,7 @@
#include "tab_card_art_rules.h" #include "tab_card_art_rules.h"
#include "tab_deck_editor.h" #include "tab_deck_editor.h"
#include "tab_deck_storage.h" #include "tab_deck_storage.h"
#include "tab_developer.h"
#include "tab_game.h" #include "tab_game.h"
#include "tab_home.h" #include "tab_home.h"
#include "tab_logs.h" #include "tab_logs.h"
@ -119,7 +120,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr), : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
tabModeration(nullptr), isLocalGame(false) tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
{ {
setElideMode(Qt::ElideRight); setElideMode(Qt::ElideRight);
setMovable(true); setMovable(true);
@ -205,6 +206,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabModeration->setCheckable(true); aTabModeration->setCheckable(true);
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration); connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
aTabDeveloper = new QAction(this);
aTabDeveloper->setCheckable(true);
connect(aTabDeveloper, &QAction::triggered, this, &TabSupervisor::actTabDeveloper);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts); &TabSupervisor::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
@ -247,6 +252,7 @@ void TabSupervisor::retranslateUi()
aTabReport->setText(tr("Report Queue")); aTabReport->setText(tr("Report Queue"));
aTabModeration->setText(tr("Moderation")); aTabModeration->setText(tr("Moderation"));
aTabCardArtRules->setText(tr("Card Art Rules")); aTabCardArtRules->setText(tr("Card Art Rules"));
aTabDeveloper->setText(tr("Developer"));
// tabs // tabs
QList<Tab *> tabs; QList<Tab *> tabs;
@ -259,6 +265,7 @@ void TabSupervisor::retranslateUi()
tabs.append(tabReport); tabs.append(tabReport);
tabs.append(tabModeration); tabs.append(tabModeration);
tabs.append(tabCardArtRules); tabs.append(tabCardArtRules);
tabs.append(tabDeveloper);
QMapIterator<int, TabRoom *> roomIterator(roomTabs); QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext()) { while (roomIterator.hasNext()) {
tabs.append(roomIterator.next().value()); tabs.append(roomIterator.next().value());
@ -528,6 +535,19 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
} }
} }
if (userInfo->user_level() & ServerInfo_User::IsDeveloper) {
tabsMenu->addSeparator();
tabsMenu->addAction(aTabDeveloper);
// Developers without moderation rights get log access through their
// own role. Moderators already have the Logs entry from above.
if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) {
tabsMenu->addAction(aTabLog);
if (SettingsCache::instance().tabs().getTabLogOpen()) {
openTabLog();
}
}
}
retranslateUi(); retranslateUi();
} }
@ -540,6 +560,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabLog = nullptr; tabLog = nullptr;
tabReport = nullptr; tabReport = nullptr;
tabModeration = nullptr; tabModeration = nullptr;
tabDeveloper = nullptr;
isLocalGame = true; isLocalGame = true;
userInfo = new ServerInfo_User; userInfo = new ServerInfo_User;
localClients = _clients; localClients = _clients;
@ -590,6 +611,9 @@ void TabSupervisor::stop()
if (tabCardArtRules) { if (tabCardArtRules) {
tabCardArtRules->close(); tabCardArtRules->close();
} }
if (tabDeveloper) {
tabDeveloper->close();
}
} }
QList<Tab *> tabsToDelete; QList<Tab *> tabsToDelete;
@ -819,7 +843,13 @@ void TabSupervisor::actTabLog(bool checked)
void TabSupervisor::openTabLog() void TabSupervisor::openTabLog()
{ {
tabLog = new TabLog(this, client); // Developers query logs through the developer command family, so tell the
// tab which family to use. The moderator family is strictly stronger, so a
// moderator who also holds the developer bit keeps the moderator path — the
// developer bit only selects the (narrowed) developer family on its own.
const bool useDeveloperCommands = (userInfo->user_level() & ServerInfo_User::IsDeveloper) &&
!(userInfo->user_level() & ServerInfo_User::IsModerator);
tabLog = new TabLog(this, client, useDeveloperCommands);
myAddTab(tabLog, aTabLog); myAddTab(tabLog, aTabLog);
connect(tabLog, &QObject::destroyed, this, [this] { connect(tabLog, &QObject::destroyed, this, [this] {
tabLog = nullptr; tabLog = nullptr;
@ -881,6 +911,27 @@ void TabSupervisor::openTabModeration(const QString &userName)
aTabModeration->setChecked(true); aTabModeration->setChecked(true);
} }
void TabSupervisor::actTabDeveloper(bool checked)
{
if (checked && !tabDeveloper) {
openTabDeveloper();
setCurrentWidget(tabDeveloper);
} else if (!checked && tabDeveloper) {
tabDeveloper->closeRequest();
}
}
void TabSupervisor::openTabDeveloper()
{
tabDeveloper = new TabDeveloper(this, client);
myAddTab(tabDeveloper, aTabDeveloper);
connect(tabDeveloper, &QObject::destroyed, this, [this] {
tabDeveloper = nullptr;
aTabDeveloper->setChecked(false);
});
aTabDeveloper->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max) void TabSupervisor::updatePingTime(int value, int max)
{ {
if (!tabServer) { if (!tabServer) {
@ -1063,6 +1114,13 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
return tab; return tab;
} }
if (focus && userListManager->isUserIgnored(receiverName)) {
QMessageBox::information(
this, tr("Ignored user"),
tr("You have ignored %1. Remove them from your ignore list to open a private chat.").arg(receiverName));
return nullptr;
}
tab = new TabMessage(this, client, *userInfo, otherUser, userOnline); tab = new TabMessage(this, client, *userInfo, otherUser, userOnline);
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft); connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
@ -1242,7 +1300,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent)
auto *tab = static_cast<Tab *>(sender()); auto *tab = static_cast<Tab *>(sender());
if (tab != currentWidget()) { if (tab != currentWidget()) {
tab->setContentsChanged(true); tab->setContentsChanged(true);
setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); setTabIcon(indexOf(tab), themePixmap(QStringLiteral("icons/tab_changed")));
} }
if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) {
QApplication::alert(this); QApplication::alert(this);
@ -1277,7 +1335,21 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
{ {
// "Ignore all private messages" silences every PM, including messages to
// already-open tabs — unlike the unregistered/non-buddy filters below,
// which only apply when creating a new tab. Messages from moderators/admins
// are exempt to ensure warnings still reach users.
QString senderName = QString::fromStdString(event.sender_name()); QString senderName = QString::fromStdString(event.sender_name());
if (SettingsCache::instance().chat().getIgnoreAllPrivateMessages()) {
const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName);
if (!onlineUserInfo) {
return;
}
const UserLevelFlags userLevel(onlineUserInfo->user_level());
if (!userLevel.testFlag(ServerInfo_User::IsModerator) && !userLevel.testFlag(ServerInfo_User::IsAdmin)) {
return;
}
}
TabMessage *tab = messageTabs.value(senderName); TabMessage *tab = messageTabs.value(senderName);
if (!tab) { if (!tab) {
tab = messageTabs.value(QString::fromStdString(event.receiver_name())); tab = messageTabs.value(QString::fromStdString(event.receiver_name()));

View file

@ -45,6 +45,7 @@ class TabReport;
class TabModeration; class TabModeration;
class TabAccount; class TabAccount;
class TabDeckEditor; class TabDeckEditor;
class TabDeveloper;
class TabLog; class TabLog;
class RoomEvent; class RoomEvent;
class GameEventContainer; class GameEventContainer;
@ -108,6 +109,7 @@ private:
TabLog *tabLog; TabLog *tabLog;
TabReport *tabReport; TabReport *tabReport;
TabModeration *tabModeration; TabModeration *tabModeration;
TabDeveloper *tabDeveloper;
QMap<int, TabRoom *> roomTabs; QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs; QMap<int, TabGame *> gameTabs;
QList<TabGame *> replayTabs; QList<TabGame *> replayTabs;
@ -117,7 +119,7 @@ private:
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration; *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration, *aTabDeveloper;
int myAddTab(Tab *tab, QAction *manager = nullptr); int myAddTab(Tab *tab, QAction *manager = nullptr);
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
@ -207,6 +209,7 @@ private slots:
void actTabLog(bool checked); void actTabLog(bool checked);
void actTabReport(bool checked); void actTabReport(bool checked);
void actTabModeration(bool checked); void actTabModeration(bool checked);
void actTabDeveloper(bool checked);
void openTabVisualDeckStorage(); void openTabVisualDeckStorage();
void openTabHome(); void openTabHome();
@ -218,6 +221,7 @@ private slots:
void openTabCardArtRules(); void openTabCardArtRules();
void openTabLog(); void openTabLog();
void openTabReport(); void openTabReport();
void openTabDeveloper();
void updateCurrent(int index); void updateCurrent(int index);
void updatePingTime(int value, int max); void updatePingTime(int value, int max);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent> #include <QKeyEvent>
@ -14,8 +15,8 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid
defaultButton = new QPushButton("", this); defaultButton = new QPushButton("", this);
lineEdit->setMinimumWidth(70); lineEdit->setMinimumWidth(70);
clearButton->setIcon(QPixmap("theme:icons/clearsearch")); clearButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
defaultButton->setIcon(QPixmap("theme:icons/update")); defaultButton->setIcon(themePixmap(QStringLiteral("icons/update")));
auto *layout = new QHBoxLayout(this); auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);

View file

@ -1,5 +1,6 @@
#include "visual_database_display_filter_toolbar_widget.h" #include "visual_database_display_filter_toolbar_widget.h"
#include "../../pixel_map_generator.h"
#include "../deck_editor/card_database_view.h" #include "../deck_editor/card_database_view.h"
#include "visual_database_display_widget.h" #include "visual_database_display_widget.h"
@ -60,22 +61,22 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg
}); });
quickFilterSaveLoadWidget = new SettingsButtonWidget(this); quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/floppy_disk")); quickFilterSaveLoadWidget->setButtonIcon(themePixmap(QStringLiteral("icons/floppy_disk")));
quickFilterNameWidget = new SettingsButtonWidget(this); quickFilterNameWidget = new SettingsButtonWidget(this);
quickFilterNameWidget->setButtonIcon(QPixmap("theme:icons/pen_to_square")); quickFilterNameWidget->setButtonIcon(themePixmap(QStringLiteral("icons/pen_to_square")));
quickFilterMainTypeWidget = new SettingsButtonWidget(this); quickFilterMainTypeWidget = new SettingsButtonWidget(this);
quickFilterMainTypeWidget->setButtonIcon(QPixmap("theme:icons/circle_half_stroke")); quickFilterMainTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/circle_half_stroke")));
quickFilterSubTypeWidget = new SettingsButtonWidget(this); quickFilterSubTypeWidget = new SettingsButtonWidget(this);
quickFilterSubTypeWidget->setButtonIcon(QPixmap("theme:icons/dragon")); quickFilterSubTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/dragon")));
quickFilterSetWidget = new SettingsButtonWidget(this); quickFilterSetWidget = new SettingsButtonWidget(this);
quickFilterSetWidget->setButtonIcon(QPixmap("theme:icons/scroll")); quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll")));
quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); quickFilterFormatLegalityWidget = new SettingsButtonWidget(this);
quickFilterFormatLegalityWidget->setButtonIcon(QPixmap("theme:icons/scale_balanced")); quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
retranslateUi(); retranslateUi();
} }

View file

@ -66,7 +66,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
setFocusProxy(searchEdit); setFocusProxy(searchEdit);
@ -121,7 +121,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
clearFilterWidget = new QToolButton(); clearFilterWidget = new QToolButton();
clearFilterWidget->setFixedSize(32, 32); clearFilterWidget->setFixedSize(32, 32);
clearFilterWidget->setIcon(QPixmap("theme:icons/delete")); clearFilterWidget->setIcon(themePixmap(QStringLiteral("icons/delete")));
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
filterModel->blockSignals(true); filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true); filterModel->filterTree()->blockSignals(true);

View file

@ -1,5 +1,6 @@
#include "visual_deck_display_options_widget.h" #include "visual_deck_display_options_widget.h"
#include "../../pixel_map_generator.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
#include <libcockatrice/utility/qt_utils.h> #include <libcockatrice/utility/qt_utils.h>
@ -47,7 +48,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
sortByLabel = new QLabel(this); sortByLabel = new QLabel(this);
sortCriteriaButton = new SettingsButtonWidget(this); sortCriteriaButton = new SettingsButtonWidget(this);
sortCriteriaButton->setButtonIcon(QPixmap("theme:icons/sort_arrow_down")); sortCriteriaButton->setButtonIcon(themePixmap(QStringLiteral("icons/sort_arrow_down")));
sortLabel = new QLabel(sortCriteriaButton); sortLabel = new QLabel(sortCriteriaButton);
sortLabel->setWordWrap(true); sortLabel->setWordWrap(true);
@ -92,7 +93,7 @@ void VisualDeckDisplayOptionsWidget::retranslateUi()
sortLabel->setText(tr("Click and drag to change the sort order within the groups")); sortLabel->setText(tr("Click and drag to change the sort order within the groups"));
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales")));
displayTypeButton->setToolTip( displayTypeButton->setToolTip(
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
} }
@ -117,11 +118,11 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType()
switch (currentDisplayType) { switch (currentDisplayType) {
case DisplayType::Flat: case DisplayType::Flat:
displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); displayTypeButton->setButtonText(tr("Toggle Layout: Flat"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scroll")));
break; break;
case DisplayType::Overlap: case DisplayType::Overlap:
displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales")));
break; break;
} }
emit displayTypeChanged(currentDisplayType); emit displayTypeChanged(currentDisplayType);

View file

@ -4,6 +4,7 @@
#include "../../../main.h" #include "../../../main.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "../../layouts/overlap_layout.h" #include "../../layouts/overlap_layout.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h" #include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../cards/deck_card_zone_display_widget.h" #include "../cards/deck_card_zone_display_widget.h"
#include "../general/layout_containers/flow_widget.h" #include "../general/layout_containers/flow_widget.h"
@ -131,7 +132,7 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
// Search button functionality // Search button functionality
searchPushButton = new CompactPushButton(searchContainer); searchPushButton = new CompactPushButton(searchContainer);
searchPushButton->setButtonIcon(QPixmap("theme:icons/search")); searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search")));
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { connect(searchPushButton, &QPushButton::clicked, this, [=, this]() {
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()}); ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
if (card) { if (card) {

View file

@ -25,7 +25,7 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) :
searchBar->setClearButtonEnabled(true); searchBar->setClearButtonEnabled(true);
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); auto help = searchBar->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
layout->addWidget(searchBar); layout->addWidget(searchBar);

View file

@ -1,6 +1,7 @@
#include "visual_deck_storage_widget.h" #include "visual_deck_storage_widget.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../quick_settings/settings_button_widget.h" #include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_color_identity_filter_widget.h"
#include "deck_preview/deck_preview_widget.h" #include "deck_preview/deck_preview_widget.h"
@ -43,7 +44,7 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
searchWidget = new VisualDeckStorageSearchWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this);
refreshButton = new QToolButton(this); refreshButton = new QToolButton(this);
refreshButton->setIcon(QPixmap("theme:icons/reload")); refreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
refreshButton->setFixedSize(32, 32); refreshButton->setFixedSize(32, 32);
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);

View file

@ -44,6 +44,7 @@
#include "intents/intent_open_server_room_by_name.h" #include "intents/intent_open_server_room_by_name.h"
#include "intents/url_parser.h" #include "intents/url_parser.h"
#include "logger.h" #include "logger.h"
#include "pixel_map_generator.h"
#include "version_string.h" #include "version_string.h"
#include "widgets/dialogs/dlg_connect.h" #include "widgets/dialogs/dlg_connect.h"
#include "widgets/server/handle_public_servers.h" #include "widgets/server/handle_public_servers.h"
@ -91,8 +92,8 @@
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
#define GITHUB_PAGES_URL "https://cockatrice.github.io" #define GITHUB_PAGES_URL "https://cockatrice.github.io"
#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors?type=c" #define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors"
#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#cockatrice" #define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#"
#define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame" #define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame"
#define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" #define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
#define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues" #define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues"
@ -271,7 +272,8 @@ void MainWindow::actAbout()
GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL + GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
"'>" + tr("F.A.Q.") + "</a><br>"), "'>" + tr("F.A.Q.") + "</a><br>"),
QMessageBox::Ok, this); QMessageBox::Ok, this);
mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); mb.setIconPixmap(
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mb.setTextInteractionFlags(Qt::TextBrowserInteraction); mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
mb.exec(); mb.exec();
} }
@ -323,7 +325,7 @@ void MainWindow::retranslateUi()
aRegister->setText(tr("&Register to server...")); aRegister->setText(tr("&Register to server..."));
aForgotPassword->setText(tr("&Restore password...")); aForgotPassword->setText(tr("&Restore password..."));
aSettings->setText(tr("&Settings...")); aSettings->setText(tr("&Settings..."));
aSettings->setIcon(QPixmap("theme:icons/settings")); aSettings->setIcon(themePixmap(QStringLiteral("icons/settings")));
aExit->setText(tr("&Exit")); aExit->setText(tr("&Exit"));
#if defined(__APPLE__) /* For OSX */ #if defined(__APPLE__) /* For OSX */
@ -682,6 +684,7 @@ void MainWindow::runFirstRunWizard()
connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground); connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground);
connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates); connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates);
connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished); connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished);
connect(this, &MainWindow::cardDatabaseUpdateProgress, wizard, &FirstRunWizard::onCardDatabaseUpdateProgress);
connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer); connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer);
connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer); connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer);
@ -816,7 +819,7 @@ void MainWindow::createTrayIcon()
trayIcon = new QSystemTrayIcon(this); trayIcon = new QSystemTrayIcon(this);
trayIcon->setContextMenu(trayIconMenu); trayIcon->setContextMenu(trayIconMenu);
trayIcon->setIcon(QPixmap("theme:cockatrice")); trayIcon->setIcon(themePixmap(QStringLiteral("cockatrice")));
trayIcon->show(); trayIcon->show();
} }
@ -843,6 +846,17 @@ void MainWindow::closeEvent(QCloseEvent *event)
} }
bClosingDown = true; bClosingDown = true;
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
if (QMessageBox::question(this, tr("Are you sure?"),
tr("A card database update is still running. Quitting now will cancel it.\n"
"Are you sure you want to quit?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
event->ignore();
bClosingDown = false;
return;
}
}
if (!tabSupervisor->close()) { if (!tabSupervisor->close()) {
event->ignore(); event->ignore();
bClosingDown = false; bClosingDown = false;
@ -1036,7 +1050,7 @@ void MainWindow::createCardUpdateProcess(bool background)
if (dir.exists(binaryName)) { if (dir.exists(binaryName)) {
updaterCmd = dir.absoluteFilePath(binaryName); updaterCmd = dir.absoluteFilePath(binaryName);
} else { // try and find the directory oracle is stored in the build directory } else { // try and find the directory Oracle is stored in the build directory
QDir findLocalDir(dir); QDir findLocalDir(dir);
findLocalDir.cdUp(); findLocalDir.cdUp();
findLocalDir.cd(getCardUpdaterBinaryName()); findLocalDir.cd(getCardUpdaterBinaryName());
@ -1057,11 +1071,45 @@ void MainWindow::createCardUpdateProcess(bool background)
if (!background) { if (!background) {
cardUpdateProcess->start(updaterCmd, QStringList()); cardUpdateProcess->start(updaterCmd, QStringList());
} else { } else {
cardUpdateOutputBuffer.clear();
connect(cardUpdateProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::cardUpdateProgressOutput);
cardUpdateProcess->start(updaterCmd, QStringList("-b")); cardUpdateProcess->start(updaterCmd, QStringList("-b"));
statusBar()->showMessage(tr("Card database update running.")); statusBar()->showMessage(tr("Card database update running."));
} }
} }
void MainWindow::cardUpdateProgressOutput()
{
if (!cardUpdateProcess) {
return;
}
cardUpdateOutputBuffer.append(cardUpdateProcess->readAllStandardOutput());
while (true) {
const int newline = cardUpdateOutputBuffer.indexOf('\n');
if (newline < 0) {
break;
}
const QByteArray line = cardUpdateOutputBuffer.left(newline).trimmed();
cardUpdateOutputBuffer.remove(0, newline + 1);
// Protocol emitted by `oracle -b`: "PROGRESS <stage> <done> <total>"
if (!line.startsWith("PROGRESS ")) {
continue;
}
const QList<QByteArray> parts = line.split(' ');
if (parts.size() != 4) {
continue;
}
bool doneOk = false;
bool totalOk = false;
const qint64 done = parts.at(2).toLongLong(&doneOk);
const qint64 total = parts.at(3).toLongLong(&totalOk);
if (!doneOk || !totalOk || done < 0 || total < 0) {
continue;
}
emit cardDatabaseUpdateProgress(QString::fromLatin1(parts.at(1)), done, total);
}
}
void MainWindow::exitCardDatabaseUpdate() void MainWindow::exitCardDatabaseUpdate()
{ {
if (!cardUpdateProcess) { if (!cardUpdateProcess) {
@ -1109,6 +1157,8 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err)
void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus) void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus)
{ {
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0); const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
if (exitStatus == QProcess::NormalExit) { if (exitStatus == QProcess::NormalExit) {
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date()); SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());

View file

@ -68,6 +68,11 @@ signals:
/** @brief Emitted after the background card-database update subprocess exits. */ /** @brief Emitted after the background card-database update subprocess exits. */
void cardDatabaseUpdateFinished(bool success); void cardDatabaseUpdateFinished(bool success);
/** @brief Emitted while the background card-database update subprocess runs.
* @p stage is one of "download", "scan" or "import"; @p done/@p total
* are byte counts for the first two stages and set indices for "import". */
void cardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
public slots: public slots:
void actCheckCardUpdates(); void actCheckCardUpdates();
void actCheckCardUpdatesBackground(); void actCheckCardUpdatesBackground();
@ -96,6 +101,7 @@ private slots:
void cardUpdateError(QProcess::ProcessError err); void cardUpdateError(QProcess::ProcessError err);
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
void cardUpdateProgressOutput();
void refreshShortcuts(); void refreshShortcuts();
void cardDatabaseLoadingFailed(); void cardDatabaseLoadingFailed();
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
@ -159,6 +165,7 @@ private:
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
bool bHasActivated, askedForDbUpdater; bool bHasActivated, askedForDbUpdater;
QProcess *cardUpdateProcess; QProcess *cardUpdateProcess;
QByteArray cardUpdateOutputBuffer;
DlgViewLog *logviewDialog; DlgViewLog *logviewDialog;
GameReplay *replay; GameReplay *replay;
DlgTipOfTheDay *tip; DlgTipOfTheDay *tip;

View file

@ -231,7 +231,7 @@ int main(int argc, char *argv[])
// These values are only used by the settings loader/saver // These values are only used by the settings loader/saver
// Wrong or outdated values are kept to not break things // Wrong or outdated values are kept to not break things
QCoreApplication::setOrganizationName("Cockatrice"); QCoreApplication::setOrganizationName("Cockatrice");
QCoreApplication::setOrganizationDomain("cockatrice.de"); QCoreApplication::setOrganizationDomain("cockatrice.github.io");
QCoreApplication::setApplicationName("Cockatrice"); QCoreApplication::setApplicationName("Cockatrice");
QCoreApplication::setApplicationVersion(VERSION_STRING); QCoreApplication::setApplicationVersion(VERSION_STRING);
@ -250,7 +250,7 @@ int main(int argc, char *argv[])
// Command-line parser // Command-line parser
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice"); parser.setApplicationDescription("Cockatrice Client");
parser.addHelpOption(); parser.addHelpOption();
parser.addVersionOption(); parser.addVersionOption();
@ -349,9 +349,9 @@ int main(int argc, char *argv[])
} }
qCInfo(MainLog) << "MainWindow constructor finished"; qCInfo(MainLog) << "MainWindow constructor finished";
ui.setWindowIcon(QPixmap("theme:cockatrice")); ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
// set name of the app desktop file; used by wayland to load the window icon // Set name of the app desktop file; used by wayland to load the window icon
QGuiApplication::setDesktopFileName("cockatrice"); QGuiApplication::setDesktopFileName("Cockatrice");
SettingsCache::instance().network().setClientID(generateClientID()); SettingsCache::instance().network().setClientID(generateClientID());

View file

@ -20,6 +20,7 @@ public:
[[nodiscard]] virtual bool getShowMessagePopup() const = 0; [[nodiscard]] virtual bool getShowMessagePopup() const = 0;
[[nodiscard]] virtual bool getShowMentionPopup() const = 0; [[nodiscard]] virtual bool getShowMentionPopup() const = 0;
[[nodiscard]] virtual bool getRoomHistory() const = 0; [[nodiscard]] virtual bool getRoomHistory() const = 0;
[[nodiscard]] virtual bool getIgnoreAllPrivateMessages() const = 0;
[[nodiscard]] virtual QString getHighlightWords() const = 0; [[nodiscard]] virtual QString getHighlightWords() const = 0;
}; };

View file

@ -253,3 +253,24 @@ PendingCommand *AbstractClient::prepareAdminCommand(const ::google::protobuf::Me
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd); c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont); return new PendingCommand(cont);
} }
PendingCommand *AbstractClient::prepareDeveloperCommand(const ::google::protobuf::Message &cmd)
{
CommandContainer cont;
DeveloperCommand *c = cont.add_developer_command();
// A developer command message may also be usable through other command
// families, so select the extension scoped to DeveloperCommand rather than
// guessing by name.
const ::google::protobuf::Descriptor *cmdDescriptor = cmd.GetDescriptor();
const ::google::protobuf::Descriptor *developerDescriptor = DeveloperCommand::descriptor();
const ::google::protobuf::FieldDescriptor *developerExtension = nullptr;
for (int i = 0; i < cmdDescriptor->extension_count(); ++i) {
if (cmdDescriptor->extension(i)->containing_type() == developerDescriptor) {
developerExtension = cmdDescriptor->extension(i);
break;
}
}
Q_ASSERT(developerExtension != nullptr);
c->GetReflection()->MutableMessage(c, developerExtension)->CopyFrom(cmd);
return new PendingCommand(cont);
}

View file

@ -173,6 +173,7 @@ public:
static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId);
static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareDeveloperCommand(const ::google::protobuf::Message &cmd);
QMap<QString, bool> clientFeatures; QMap<QString, bool> clientFeatures;
}; };

Some files were not shown because too many files have changed in this diff Show more