Compare commits

...

141 Commits

Author SHA1 Message Date
RickyRister
b1fca404b7 Bump to 2.10.2 for hotfix release (#5785) 2025-04-03 00:24:11 -04:00
HypersonicWalrus
b6e6328e6a Modified setup_addfirstadmin script to bring it up to date to current cockatrice_users table (#5784)
Co-authored-by: Ben Boelens <bboelens@pop-os.tailc0350.ts.net>
2025-04-03 00:18:31 -04:00
tooomm
ecf0327378 Improve list of release binaries (#5783) 2025-04-03 00:17:44 -04:00
github-actions[bot]
787c551f5f Update translation source strings (#5781)
Co-authored-by: github-actions <github-actions@github.com>
2025-04-03 00:17:25 -04:00
RickyRister
d662152088 Change default log level to info (#5779)
* update qtlogging.ini

* bump some qCDebug to qCInfo and qCWarning
2025-04-03 00:17:10 -04:00
Basile Clement
2fcdb52157 fix: Use isRebalanced to detect Arena cards (#5778)
* fix: Use isRebalanced to detect Arena cards

In #5759 we introduced a setting (off by default) to disable the use of
Arena cards. This was done by checking the `isOnlineOnly` property of
the card, which accidentally also disabled online *printings* of cards
that otherwise exist in paper (e.g. Vintage Masters).

This PR does the same thing but uses the `isRebalanced` property
instead, which is `true` for Arena cards only and should have been used
from the start. This setting does not impact online-only printings such
as Vintage Masters. The settings is still on by default.

* Update setting to mention Alchemy rather than Arena
2025-04-03 00:16:38 -04:00
ZeldaZach
70f2a32fad Bump to 2.11.0 for beta releases 2025-03-27 21:34:03 -04:00
github-actions[bot]
37356317a4 Update translation files (#5775)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-27 21:31:05 -04:00
RickyRister
08f3a56285 Fix crash when right click floating card info window (#5773) 2025-03-27 00:54:34 +00:00
RickyRister
5af71d1c2e Hardcode default log level for FlowWidget/FlowLayout (#5769) 2025-03-26 01:23:23 +00:00
Basile Clement
1ada5ea424 fix: Always prefer local cards if available (#5762)
* Try to better reproduce pre-provider ID behavior

If "override all card art with personal preference" setting is set, look
for custom art for all sets instead of just the most preferred set.

* Warning when using both custom art and the printing selector

* QDirIterator::nextFileInfo is Qt 6.3+

* Translation
2025-03-26 01:23:09 +00:00
transifex-integration[bot]
91ee6097d2 Translate oracle/oracle_en@source.ts in it (#5770)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-03-24 22:11:47 +00:00
Zach H
a4b0cddcf8 Revert "Disable CardMenu iff no items selected (#5376)" (#5768)
This reverts commit b4036c8671.
2025-03-23 19:04:24 -04:00
RickyRister
9bc6ae1567 Fix delete action in filters not working (#5765)
* Fix delete action in filters not working

* move filterRemove under slots
2025-03-23 12:03:56 -04:00
Basile Clement
c71685b261 Add option to disable card rounding (#5760)
* Add option to disable card rounding

* Effing mocks

* format

* Get rid of cardCornerRadius property
2025-03-22 01:07:52 -04:00
Basile Clement
0ae7d01234 Hide arena only cards (#5759)
* Add settings (default: true) to ignore online-only cards

* Use QAbstractButton::toggled

Also, fix dbconverter build

* Mocks mocks mocks

* Update dlg_manage_sets.cpp

* translations

* Update dlg_manage_sets.cpp

---------

Co-authored-by: Zach H <zahalpern+github@gmail.com>
2025-03-22 01:07:42 -04:00
RickyRister
345606846f Enable shortcuts for the remaining export deck actions (#5761) 2025-03-21 02:49:02 +00:00
RickyRister
9decf78d2d Fix typo in comment about accepted decklist file formats (#5754) 2025-03-21 00:31:38 +00:00
Basile Clement
a6f2e69e1a vds: Allow editing tags more than once (#5752)
`refreshTags` is not connecting the signal to open the dialog to edit
the tags, so tags can only be edited once for a given deck.

Fix by only having the logic for creating the "Edit tags" button once
and call it from `connectDeckList`.
2025-03-21 00:31:25 +00:00
Basile Clement
76fa87c63e Fix StackZone crash when divideCardSpaceInZone overflows (#5751)
The divideCardSpaceInZone function introduced in #4930 is buggy and
sometimes returns an index that is too large for the current zone, which
causes us to call `cards.at(index)` with an `index` that's bigger than
the amount of cards.

This is the bug that #5609 intended to fix but was improperly diagnosed.
Remove part of #5609 as the cases it is guarding against (e.g. null card
pointer) cannot actually happen.
2025-03-21 00:30:46 +00:00
RickyRister
2e01dfd23a Remember past entries in "reveal card until X" window (#5755) 2025-03-21 00:29:59 +00:00
RickyRister
99376e75d6 Support exporting to decklist.xyz website (#5756)
* Support exporting to decklist.xyz

* fix typo
2025-03-21 00:28:15 +00:00
Basile Clement
be28d50997 Revert "Use native hover events (#5722)" (#5757)
This reverts commit e4f40a82a2.

This change had unintended consequences in the hover behavior, reverting
for now.
2025-03-21 00:25:20 +00:00
Basile Clement
d03f5388d4 Update translations (#5758) 2025-03-21 00:23:26 +00:00
Zach H
48123c8822 Revert "Display visual feedback of where cards will go (#5737)" (#5750)
This reverts commit a7641a571f.
2025-03-19 01:53:35 +00:00
RickyRister
0fa744f6ec Consolidate accepted decklist file extensions (#5749)
* Consolidate accepted decklist file extensions

* rename the other const
2025-03-19 01:53:14 +00:00
RickyRister
42301d4f1a Filter out non-deck files when building VDS (#5748) 2025-03-18 22:22:36 +00:00
RickyRister
6c19254abd Fix AttachTo tokens not having card info (#5747) 2025-03-18 22:22:16 +00:00
RickyRister
b5c5d221c4 Remove redundant "show unused color identities" settings (#5745)
* move setting to vds settings menu

* emit signal on change

* rename setting
2025-03-18 22:21:28 +00:00
Basile Clement
c219d8bdbb hotfix: Remove menus when closing game (#5744)
Version of #5740 that doesn't leave freed `QMenu`s lying around.
2025-03-17 22:54:16 -04:00
Basile Clement
4812508afc DeckEditor: Initialize the modified flag (#5743)
C++ does not require compilers to zero-initialize value types, so
depending on the platform (here: Linux), the deck editor starts up with
an uninitialized value in the `modified` flag, which is usually not
zero.
2025-03-17 21:43:14 -04:00
RickyRister
57b9f0e54c Add CONFIGURE_DEPENDS to the cmake (#5739) 2025-03-17 02:43:11 +00:00
RickyRister
0d2061365c Fix edit deck in clipboard clearing values (#5732)
* Fix edit deck in clipboard clearing values

* fix build failures
2025-03-16 23:30:12 +00:00
RickyRister
4d8a124822 Rename save to clipboard actions in DeckPreviewWidget (#5738) 2025-03-16 23:19:57 +00:00
Basile Clement
a7641a571f Display visual feedback of where cards will go (#5737)
This is part of the code from #4974, including an improved drag-and-drop
API and its use to display visual feedback of card destination on the
board.

It does not include the improved logic for pushing cards around as I
still need to figure out edge cases there - the logic for choosing where
cards go is not changed, so some of the artifacts described in #4817
and #4975 (particularly around multi-card) are still present.
2025-03-16 23:19:39 +00:00
BruebachL
bd28e04635 Reintroduce unused color identity opacity (#5733)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-16 23:05:38 +00:00
Basile Clement
37382dea44 Close the TabGames when closing the TabSupervisor (#5735)
* Close the `TabGame`s when closing the `TabSupervisor`

This ensures that we go through the same code path (in terms of Qt
events) when closing the whole supervisor as when closing a single tab.
Also, use the `close` event instead of the `hide` event to detect when
we are closing a game.

Fixes #5697

* Compat with old Qt versions

* Old Qt, reloaded

* Review: use hideEvent and call super
2025-03-16 23:05:04 +00:00
Basile Clement
57a8960841 Add declaration for setAttrRecur (#5734) 2025-03-16 23:02:31 +00:00
RickyRister
6b4ae8308a Reduce tag display widget spacing (#5731)
* Reduce tag display widget spacing

* Reduce bottom margin in deck dock
2025-03-16 23:02:06 +00:00
Basile Clement
2739550087 Use enum for ThemeManager brushes (#5730)
* Use enum for ThemeManager brushes

This patch introduces an enum to distinguish the different brushes that
can be set by the theme (hand, stack, etc.) and generic functions taking
the enum rather than having one copy of each function for each brush.

This is preliminary work before merging StackZone and HandZone to
simplify #4974.

* Include <array> header

* Header spacing
2025-03-16 23:01:25 +00:00
Basile Clement
4ada011632 game: Automatic update of arrow position (#5729)
Currently, zones must keep track of which cards they move in order to
manually call `updatePath` on arrows.

This patch sets the `ItemSendsScenePositionChanges` flag on
`ArrowTarget`s to automatically update arrow positions without requiring
zones to keep track of that information.
2025-03-16 22:58:06 +00:00
RickyRister
c99afe7956 Optimize cipt parsing by early returning (#5727) 2025-03-15 21:23:41 +00:00
RickyRister
b58b85dc0f Re-add old names for mana value property to oracle (#5711) 2025-03-15 19:13:13 +00:00
BruebachL
a407c8b956 Reintroduce ability to display unused mana symbol widgets. (#5726)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-15 19:11:46 +00:00
Basile Clement
1851f71850 Remove revealedCard flag from CardItem (#5723)
It is no longer used since #5254.
2025-03-15 19:09:14 +00:00
Basile Clement
e4f40a82a2 Use native hover events (#5722)
* Use native hover events

* Update cockatrice/src/game/cards/abstract_card_item.cpp

* Reorder

---------

Co-authored-by: Zach H <zahalpern+github@gmail.com>
2025-03-15 19:07:51 +00:00
Basile Clement
b9900e67a6 nix: Add development utilities to shell.nix (#5725)
- Remove hardening flags to allow debug builds (otherwise GCC complains
   because nix adds the FORTIFY_SOURCE flag, which is not compatible
   with -O0)

 - Allow ninja as build system

 - Add clang-tools dependency for LSP support
2025-03-15 19:03:26 +00:00
RickyRister
7d558edb3e Fix banner and tags not resetting on blank new deck (#5721)
* Fix bannerWidget not resetting when opening new blank deck

* also reset tags
2025-03-15 18:44:51 +00:00
RickyRister
eb4b1c2a07 Fix extra .cod in "save deck as" default name (#5720) 2025-03-15 18:44:03 +00:00
RickyRister
087f88146d Make internal updater failure message more user-friendly (#5718) 2025-03-15 05:19:07 +00:00
RickyRister
3a11ccb854 Update cipt parsing (#5712)
* refactor

* move thing out

* write unit tests

* get thing to work

* optimization?

* fix build failure
2025-03-14 21:44:13 -04:00
tooomm
068465143b Update CONTRIBUTING file (#5701)
* Update CONTRIBUTING.md

* cleanup

* Update CONTRIBUTING.md
2025-03-14 21:43:43 -04:00
tooomm
1f0846297f websocket is our default port/connection (#5679) 2025-03-14 21:43:11 -04:00
RickyRister
4a0e0ed954 Automatically find all files for cockatrice_SOURCES (#5716)
* Use GLOB_RECURSIVE to find all source files

* fix code style

---------

Co-authored-by: Zach H <zahalpern+github@gmail.com>
2025-03-14 21:42:56 -04:00
RickyRister
4b7d1ebb59 Refactor: split card_database into two files (#5715)
* make the duplicate

* restore original

* Refactor: split card_database into two files
2025-03-14 00:02:10 +00:00
RickyRister
ec536126b9 Compute deck hashes lazily (#5707)
* Calculate deck hashes lazily

* rename
2025-03-11 21:43:21 -04:00
Zach H
9b00bdcaea Merge pull request #5710 from lilyhuang-github/i18n
Change prebuild.js to allow i18n-default.json indentation to exist
2025-03-11 21:00:53 -04:00
Lily
da17c68830 Change prebuild.js to allow i18n-default.json indentation to exist 2025-03-11 19:59:20 -04:00
Zach H
badfb483b2 Merge pull request #5703 from RickyRister/5699-fix-printing
Show correct printing for top card of library
2025-03-10 20:45:05 -04:00
Zach H
fafe636b7c Merge pull request #5702 from RickyRister/rename-export-actions
Rename some save to clipboard actions
2025-03-10 20:44:33 -04:00
RickyRister
ef15aa2dcd Show correct printing for top card of library 2025-03-09 05:40:03 -07:00
RickyRister
2d44c9ad2f Rename some save to clipboard actions 2025-03-08 20:02:32 -08:00
RickyRister
0a1d0f650f Enable shortcuts for new save to clipboard actions (#5700)
* Enable shortcuts for new save to clipboard actions

* rename shortcut
2025-03-08 22:41:58 -05:00
RickyRister
2ba7c1ff9a Fix incorrect deck modified state (#5698) 2025-03-08 02:16:59 +00:00
RickyRister
0ecf6298a3 Add actions for shuffle top/bottom X cards (#5695)
* Add actions for shuffle top/bottom X cards

* fix typo

* move shuffle actions into existing menus
2025-03-08 01:48:48 +00:00
RickyRister
1d11bb19b8 Fix view bottom cards using the wrong default (#5696) 2025-03-07 16:17:40 -05:00
RickyRister
ff7f31ca33 add "Set Banner Card" action to VDS right-click menu (#5692) 2025-03-05 21:05:34 -05:00
RickyRister
6bb9ae92bf Update recently opened decks regardless of where the deck is opened from (#5691) 2025-03-05 21:04:53 -05:00
ZeldaZach
5238087ddf Parent bannerCardLabel to avoid popups 2025-03-04 22:45:53 -05:00
RickyRister
08bb18cefe Fix VDS filters not applying after refresh (#5662)
* reapply sort and filters after each reset

* fix unflatten folder still not applying afterwards
2025-03-05 00:57:28 +00:00
RickyRister
ecbdd32a2d Reduce redundant recursion in VDS (#5664)
* remove recursion from flattenFolderStructure

findChildren is already recursive by default

* only trigger the top-level updateVisibility on filter update

Every folder widget was connecting the filter update signals to their updateVisibility, but updateVisibility is already recursive.
That means a bunch of redundant updateVisibility calls happen every time a filter update signal is emitted

* reduce redundant recursion in updateVisibility

findChildren is recursive by default, so only the top-level updateVisibility needs to loop through the found children

* delete now-unused signals
2025-03-05 00:56:31 +00:00
RickyRister
2f415dcc6e Add action to Edit Deck via Clipboard (#5681)
* implement functionality in dlg

* add action to deck editor

* refactor and comments

* is this refactor even a good idea?

* remove the friend class stuff

* reorder

* add option for not annotated
2025-03-05 00:55:05 +00:00
BruebachL
8fc1b22889 Use the new mana symbols. (#5687)
* Use the new mana symbols.

* Fixup some thangs.

* Lint.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-05 00:53:42 +00:00
BruebachL
85a50ce9d5 Generic mana symbols. (#5685)
* Generic mana symbols.

* Update black mana symbol skull and symbol color.

* Update white mana symbol contrast.

* Update black mana symbol contrast.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-03 13:12:09 -05:00
BruebachL
b706e26a32 Fix image quality when fractional scaling is applied. (#5684)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-03 08:40:12 -05:00
BruebachL
15725c67c7 Fix banner widget buddy being uninitialized. (#5686)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-03 08:38:27 -05:00
RickyRister
d8da4473e2 add rename and delete actions to VDS right-click menu (#5683) 2025-03-03 08:35:50 -05:00
RickyRister
e1964f21de Fix memory leaks from DeckLoader usage (#5665)
* add comment

* stack allocate DeckLoader for loading tags

* deckModel now takes ownership of DeckLoader

* fix remaining

* add comment
2025-03-02 18:57:30 -05:00
RickyRister
87c5d07807 Switch current tab when opening a single-instance tab (#5651) 2025-03-02 09:30:55 -05:00
lilyhuang-github
3d0f4868df Focus tab for quit game dialog (#5670) 2025-03-02 09:29:17 -05:00
BruebachL
56bd11794e Set modification state correctly. (#5678)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-02 09:28:16 -05:00
BruebachL
e541b9d572 Silence picture loader warnings for null pixmaps by introducing checks. (#5677)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-02 09:27:52 -05:00
RickyRister
a7e8c1f59b Make BannerWidget's dropdown icon more robust (#5676)
* Make BannerWidget's dropdown icon more robust

* use isHidden

Otherwise, it doesn't work correctly if the BannerWidget is offscreen

* don't show icon if there's no buddy
2025-03-02 09:26:03 -05:00
BruebachL
ec452aabe2 Fallback to regular card name search in case the providerId one fails for the database display widget. (#5673)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-03-01 09:22:00 -05:00
RickyRister
2a9599eed8 refactor saveDeckToClipboard in AbstractTabDeckEditor (#5671)
* refactor saveDeckToClipboard in AbstractTabDeckEditor

* make deckloader functions const

* use const

* move method into DeckLoader
2025-03-01 09:21:31 -05:00
RickyRister
58a2b7ff39 Fix move to top of library shuffling an extra card (#5672)
* Fix move to top of library shuffling an extra card

* Update cockatrice/src/game/player/player.cpp

---------

Co-authored-by: ebbit1q <ebbit1q@gmail.com>
2025-03-01 13:32:07 +01:00
RickyRister
448903efe3 Add tip of the day for Expand Card View Window (#5666) 2025-02-28 11:29:09 -05:00
BruebachL
d76e8f9146 Clamp all DeckPreviewWidget children to the card size on resize. (#5669)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-28 11:10:40 -05:00
BruebachL
3620664a9f Cache correct providerId cards on deck load (#5668)
* Implement new method for DeckList to return cardlist with providerId, a new carddatabase method to fetch a cardlist with name and providerId and changed PictureLoader to use providerId versions of cards for caching.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-28 11:10:06 -05:00
lilyhuang-github
7ff43f15fc Add hint on version downloading (#5663) 2025-02-27 23:34:32 -05:00
lilyhuang-github
e271e6ecf8 Implement advertisement to FAQ page align right (#5657) 2025-02-27 23:34:06 -05:00
BruebachL
93d28717e0 Abstract deck editor (#5646)
* Generify TabDeckEditor.

* Connect dockTopLevelChanged signals.

* Connect eventFilters.

* Remove comments.

* Fix ze build (accidentally deleted a line)

* Fix some pointer chaining.

* Be a lot saner about some signals/slots, as in, individual Deck Editor widgets now internally determine their CardInfo and then simply communicate this to the DeckEditor

* Lint.

* DeckDock can handle its own menu.

* DeckDock can handle its own decrement.

* DeckDock now notifies the deck editor on deck change, instead of individually modifying menu items and modification status.

* Rename.

* Include pixelmap generator for icon.

* Directly use an AbstractTabDeckEditor as parent.

* Move clearing database filter into signal/slot relation.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-27 10:57:58 -05:00
RickyRister
6df97a156f Change "include folder name in VDS search" into a setting (#5659)
* add new setting

* implement thing
2025-02-26 12:01:30 -05:00
BruebachL
05d06f9016 Sort Tags in TagFilterWidget (#5660)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-26 11:59:59 -05:00
RickyRister
e8574641b0 use new rename icon in replays tab (#5658) 2025-02-26 00:14:27 -05:00
RickyRister
9ac13018c6 include folder name in VDS search when folders are enabled (#5637) 2025-02-25 18:39:34 -05:00
RickyRister
06b25f1cfc add "edit tags" to VDS right-click menu (#5631)
* refactor: move openTagEditDlg up a level

* add edit tags to menu

* set DeleteOnClose attribute on menu

* fix build failure
2025-02-25 18:38:55 -05:00
RickyRister
6f5d369416 Add folder dropdown icons to VDS (#5632)
* add svg

* update pixmap cache

* get icon to work

* hide icon when not clickable

* use consistent naming

* use expandOnly because apparently that leads to higher image quality
2025-02-25 18:36:48 -05:00
RickyRister
4543038fd8 add "open in deck editor" to VDS right-click menu (#5634) 2025-02-25 18:35:56 -05:00
RickyRister
345f8b772c show error message when open deck fails in VDS (#5642) 2025-02-25 18:33:16 -05:00
RickyRister
47311b1dfd Expand/shrink card view window on double click (#5652) 2025-02-25 18:32:45 -05:00
RickyRister
21e22ed5fb Destroy VDS in game lobby once game starts (#5643)
* push back setting change

* rename method and make it public

* destroy vds on game start
2025-02-25 18:31:44 -05:00
RickyRister
0d6497fcdc Fix banner card changing when opening deck in new tab (#5649) 2025-02-25 18:30:53 -05:00
RickyRister
bfaeeb5aea fix transform from non-table zones not moving card (#5648) 2025-02-25 18:30:38 -05:00
RickyRister
b46667f6db Fix memory leak when refreshing VDS (#5647)
* parent deckLoader

* leave comment
2025-02-25 18:30:20 -05:00
RickyRister
49932ee6f8 Fix segfault when oracle reads card without "num" field (#5654) 2025-02-25 18:29:43 -05:00
RickyRister
57e37e8f4d Allow loading local xml file in oracle (#5655) 2025-02-25 18:29:27 -05:00
RickyRister
af68a95964 Add rename action to deck storage tab (#5656)
* add icon

* add rename action
2025-02-25 18:29:07 -05:00
RickyRister
959a268f91 Mark deck as modified when banner card is changed (#5641)
* Mark deck as modified when banner card is changed

* set modified inside setBannerCard
2025-02-22 13:56:48 +00:00
RickyRister
77a3515470 Always download macOS 13 version on intel macs (#5630)
* Always download macOS 13 version for intel macs

* use contains instead of regex
2025-02-18 22:27:17 +00:00
BruebachL
6a008acb2b Enhance card update error description. (#5638)
* Enhance card update error description.

* Enhance card update error description.

* Lint.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-18 22:20:06 +00:00
RickyRister
247e1aff83 Also parse "colors" field in xml (#5635) 2025-02-18 20:04:59 +00:00
RickyRister
3df4efebaa Revert file watcher in VDS (#5636) 2025-02-18 20:04:37 +00:00
RickyRister
5c8d1f3cff Make AttachTo tokens work from non-table zones (#5629)
* move card to play before creating attached token

* leave comment

* hardcode createCard target zone to table

To get attached token from graveyard/exile to work
2025-02-16 17:02:45 -05:00
Zach H
01d5e58a5f Change Sorting Order of User List (#5626)
* 1) Online Users > Offline Users
* 2) Admins, judge/vip/donator status ignored
* 3) Moderators, judge/vip/donator status ignored
* 4) Judges
* 5) VIPs
* 6) Donators
* 7) Everyone else
2025-02-16 04:35:28 +00:00
RickyRister
a28300ac42 add "save deck to clipboard" to VDS right click menu (#5625) 2025-02-16 04:18:47 +00:00
RickyRister
0666483756 refactor saveDeckToClipboard in TabDeckEditor (#5623) 2025-02-16 02:43:44 +00:00
RickyRister
abca5514af support right-click menu in VDS (#5622)
* support right-click menu in VDS

* move methods around
2025-02-16 02:43:20 +00:00
RickyRister
2247c66ea6 refactor how double click signal gets passed up in VDS (#5621) 2025-02-15 19:06:11 -05:00
RickyRister
3b638598ad fix load remote deck window being empty (#5613) 2025-02-15 21:05:33 +00:00
RickyRister
63e3e3ceb1 refactor and add missing log categories to ReleaseChannel (#5615)
* adding missing log categories

* refactor version lookup

* refactor OS checking code
2025-02-15 21:05:12 +00:00
BruebachL
23f4c9c4e4 Tags in deck editor (#5608) 2025-02-12 03:18:00 +00:00
Zach H
356b00e8c7 Fix Ricky Crash (#5609) 2025-02-12 01:39:41 +00:00
RickyRister
8916e049bd fix printing selector dropdown not working on mac (#5606) 2025-02-11 04:42:31 +00:00
ZeldaZach
287b4a5597 Bump to 2.10.1 2025-02-10 23:26:35 -05:00
RickyRister
d77ee00e70 Fix crash when VDS show tags is unchecked (#5605)
* Fix crash when VDS show tags is unchecked

* revert the refactor since I don't know if there's a reason they did it that way
2025-02-11 04:24:08 +00:00
ZeldaZach
18ac4c2bd4 Set Release Name: Omenpath 2025-02-10 16:41:18 -05:00
Zach H
2dc614c6b7 Add new Tips of the Day (#5603)
- Thanks WargiCorgi!
2025-02-10 21:22:26 +00:00
BruebachL
7c9bf75393 Overhaul quick settings for VDS and PrintingSelector (#5602)
* Move show folders option next to the search bar.

* Add a new settings button and settings popup, move the folder visibility checkbox there and the ability to hide tags.

* Make popup not close when interacting with child widgets.

* Fix mocks.

* Include cog icon.

* Move PrintingSelector Display options to new quick settings widget.

* Adjust size before first show so as to not overflow.

* Add option to hide card size slider in VDS.

* Qt5 support.

* Fix some warnings by containerizing layouts because addChildLayout is silly.

* Fix an incorrect slot/signal assignment.

* Correct sub-categories for settings to persist them.

* Shuffle some slots and signals around to distinguish between the tag filter and the tags on the deck preview widgets.

* Add a quick setting to draw unused color identities and center them.

* Respect the setting on startup.

* Move card size slider to the quick settings menu.

* Move PrintingSelector card size slider to quick menu, adjust other layout from other options.

* Improve layout, add a gray border.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-10 16:50:08 +00:00
transifex-integration[bot]
d1102939a2 Translate cockatrice_en@source.ts in pt_BR (#5601)
100% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-02-09 22:12:38 +00:00
Zach H
cb060f43b5 Prevent server crash if DB is down and game is attempted to be created (#5600) 2025-02-09 22:11:00 +00:00
transifex-integration[bot]
80bd783d54 Updates for project Cockatrice and language de (#5595)
* Translate cockatrice/cockatrice_en@source.ts in de

100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

* Translate cockatrice/cockatrice_en@source.ts in de

100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-02-09 19:16:27 +01:00
tooomm
82b147d235 uniform artifact naming (#5592) 2025-02-09 12:40:07 +00:00
Zach H
6944f5f81c Put Logging config on MacOS (#5591) 2025-02-09 12:39:09 +00:00
danbopes
8cbfe85ed4 Fix db reconnecting issues (#5590)
See:

c1b0d50237 (diff-02a32f437187bd4cbfab74877100fee0cfc669dab2c05418681a3557c2cf73f2R109)

We should be checking to see if the query is notActive. In this case, we're literally closing and reopening the connection to the database every time `checkSql()` is called, which is called in numerous places.
2025-02-09 12:12:16 +00:00
Zach H
eb2c71d381 Fix MacOS Finding releases (#5589) 2025-02-09 02:26:04 -05:00
RickyRister
86161185d9 Use local-aware compare in VDS file name sort (#5588) 2025-02-09 04:38:48 +00:00
RickyRister
79bf3adb2a Re-sort decks in VDS after toggling show folders (#5587) 2025-02-09 04:38:22 +00:00
Zach H
07ca243d48 Update QT_LOGGING_CONF if not set (#5578) 2025-02-08 16:10:45 +00:00
Zach H
edaca772c5 Support logging config on install (#5577)
* Support logging config on install

* Support logging config on install
2025-02-08 16:10:37 +00:00
ZeldaZach
b30b72b7c3 Fix building & logging out 2025-02-07 14:19:53 -05:00
260 changed files with 53998 additions and 39985 deletions

View File

@@ -4,24 +4,30 @@
git push -d origin --REPLACE-WITH-BETA-LIST--
-->
<!-- This list of binaries should be updated every time the CI is changed to
include different targets -->
<!-- This list of binaries should be updated every time the CI is changed to include all targets -->
<pre>
<b>Pre-compiled binaries we serve:</b>
- <kbd>Windows 10+</kbd>
- <kbd>Windows 7+</kbd>
- <kbd>macOS 14+</kbd> ("Sonoma") / Apple M
- <kbd>macOS 13+</kbd> ("Ventura") / Intel
- <kbd>Ubuntu 24.04 LTS</kbd> ("Noble Numbat")
- <kbd>Ubuntu 22.04 LTS</kbd> ("Jammy Jellyfish")
- <kbd>Ubuntu 20.04 LTS</kbd> ("Focal Fossa")
- <kbd>Debian 12</kbd> ("Bookworm")
- <kbd>Debian 11</kbd> ("Bullseye")
- <kbd>Fedora 41</kbd>
- <kbd>Fedora 40</kbd>
<i>We are also packaged in <kbd>Arch Linux</kbd>'s official "extra" repository, courtesy of @FFY00</i>
<i>General Linux support is available via a <kbd>flatpak</kbd> package (Flathub)</i>
Available pre-compiled binaries for installation:
<b>Windows</b>
<kbd>Windows 10+</kbd>
<kbd>Windows 7+</kbd>
<b>macOS</b>
<kbd>macOS 15+</kbd> <sub><i>Sequoia</i></sub> <sub>Apple M</sub>
<kbd>macOS 14+</kbd> <sub><i>Sonoma</i></sub> <sub>Apple M</sub>
<kbd>macOS 13+</kbd> <sub><i>Ventura</i></sub> <sub>Intel</sub>
<b>Linux</b>
• <kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
• <kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
• <kbd>Ubuntu 20.04 LTS</kbd> <sub><i>Focal Fossa</i></sub>
• <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
• <kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
• <kbd>Fedora 41</kbd>
• <kbd>Fedora 40</kbd>
<sub>We are also packaged in <kbd>Arch Linux</kbd>'s <a href="https://archlinux.org/packages/extra/x86_64/cockatrice">official extra repository</a>, courtesy of @FFY00.</sub>
<sub>General Linux support is available via a <kbd>flatpak</kbd> package at <a href="https://flathub.org/apps/io.github.Cockatrice.cockatrice">Flathub</a>!</sub>
</pre>

View File

@@ -7,32 +7,33 @@
<br>
# Contributing to Cockatrice #
First off, thanks for taking the time to contribute to our project! 🎉 ❤ ️✨
The following is a set of guidelines for contributing to Cockatrice. These are
mostly guidelines, not rules. Use your best judgment, and feel free to propose
changes to this document in a pull request.
First off, thanks for taking the time and considering to lend a helping hand to our project! 🎉 ❤ ️✨
> [!NOTE]
> The following is a set of guidelines for contributing to Cockatrice.
> These are mostly guidelines, not rules. Use your best judgment, and feel free to
> propose changes to this document in a pull request.
>
> [![Discord](
> https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](
> https://discord.gg/3Z9yzmA)
> If you'd like to ask questions, get advice, or just want to say "Hi",
> the Cockatrice Development Team uses [Discord](https://discord.gg/ZASRzKu)
> for communications and you can reach out in the `#dev` channel.
# Recommended Setups #
For those developers who like the Linux or MacOS environment, many of our
For those developers on **Linux** or **macOS** environment, many of our
developers like working with a nifty program called [CLion](
https://www.jetbrains.com/clion/). The program's a great asset and one of the
best tools you'll find on these systems, but you're welcomed to use any IDE
you most enjoy.
https://www.jetbrains.com/clion/). The program is a great asset and one of the
best tools you'll find on these systems.
Developers who like Windows development tend to find [Visual Studio](
Developers on **Windows** systems tend to find [Visual Studio](
https://www.visualstudio.com/) the best tool for the job.
[![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white&color=7289da)](https://discord.gg/ZASRzKu)
[![Gitter Chat](https://img.shields.io/gitter/room/Cockatrice/Cockatrice.svg)](https://gitter.im/Cockatrice/Cockatrice)
If you'd like to ask questions, get advice, or just want to say hi,
the Cockatrice Development Team uses [Discord](https://discord.gg/ZASRzKu)
for communications in the #dev channel. If you're not into Discord, we also
have a [Gitter](https://gitter.im/Cockatrice/Cockatrice) channel available,
albeit slightly less active.
But you're welcomed to use any IDE you enjoy most of course!
# Code Style Guide #
@@ -54,7 +55,7 @@ The message will look like this:
*** Then commit and push those changes to this branch. ***
*** Check our CONTRIBUTING.md file for more details. ***
*** ***
*** Thank you ❤️ ***
*** Thank you ❤️ ***
*** ***
***********************************************************
```
@@ -64,7 +65,7 @@ information on our formatting guidelines.
### Compatibility ###
Cockatrice is currently compiled on all platforms using <kbd>C++11</kbd>.
Cockatrice is currently compiled on all platforms using <kbd>C++20</kbd>.
You'll notice <kbd>C++03</kbd> code throughout the codebase. Please feel free
to help convert it over!
@@ -78,11 +79,12 @@ or other appropriate conversion.
### Formatting ###
The handy tool `clang-format` can format your code for you, it is available for
almost any environment. A special `.clang-format` configuration file is
almost any environment. A special [`.clang-format`](
https://github.com/Cockatrice/Cockatrice/blob/master/.clang-format) configuration file is
included in the project and is used to format your code.
We've also included a bash script, `format.sh`, that will use clang-format to
format all files in your pr in one go. Use `./format.sh --help` to show a full
format all files in your PR in one go. Use `./format.sh --help` to show a full
help page.
To run clang-format on a single source file simply use the command
@@ -90,10 +92,10 @@ To run clang-format on a single source file simply use the command
clang-format with a specific version number appended,
`find /usr/bin -name clang-format*` should find it for you)
See [the clang-format documentation](
See the [clang-format documentation](
https://clang.llvm.org/docs/ClangFormat.html) for more information about the tool.
#### Header files ####
#### Header Files ####
Use header files with the extension `.h` and source files with the extension
`.cpp`.
@@ -168,10 +170,10 @@ braces around single line statements is preferred.
See the following example:
```c++
int main()
{ // function or class: own line
if (someCondition) { // control statement: same line
doSomething(); // single line statement, braces preferred
} else if (someOtherCondition1) { // else goes on the same line as a closing brace
{ // function or class: own line
if (someCondition) { // control statement: same line
doSomething(); // single line statement, braces preferred
} else if (someOtherCondition1) { // else goes on the same line as a closing brace
for (int i = 0; i < 100; i++) {
doSomethingElse();
}
@@ -234,7 +236,7 @@ mutating objects.)
When pointers can't be avoided, try to use a smart pointer of some sort, such
as `QScopedPointer`, or, less preferably, `QSharedPointer`.
### Database migrations ###
### Database Migrations ###
The servatrice database's schema can be found at `servatrice/servatrice.sql`.
Everytime the schema gets modified, some other steps are due:
@@ -255,7 +257,7 @@ Ensure that the migration produces the expected effects; e.g. if you add a
new column, make sure the migration places it in the same order as
servatrice.sql.
### Protocol buffer ###
### Protocol Buffer ###
Cockatrice and Servatrice exchange data using binary messages. The syntax of
these messages is defined in the `proto` files in the `common/pb` folder. These
@@ -268,6 +270,7 @@ new clients incompatible to the old server and vice versa.
You can find more information on how we use Protobuf on [our wiki!](
https://github.com/Cockatrice/Cockatrice/wiki/Client-server-protocol)
# Reviewing Pull Requests #
After you have finished your changes to the project you should put them on a
@@ -286,6 +289,7 @@ all changes have been approved your pull request will be squashed into a single
commit and merged into the master branch by a team member. Your change will then
be included in the next release 👍
# Translations #
Basic workflow for translations:
@@ -294,16 +298,16 @@ Basic workflow for translations:
3. Maintainer verifies and merges the change;
4. Transifex picks up the new files from GitHub automatically;
5. Translators translate the new untranslated strings on Transifex;
6. Before a release, a maintainer fetches the updated translations from Transifex.
6. Before a release, a maintainer fetches the newest translations from Transifex.
### Using Translations (for developers) ###
All user interface strings inside Cockatrice's source code must be written
in English (US).
Translations to other languages are managed using [Transifex](
https://www.transifex.com/projects/p/cockatrice/).
https://transifex.com/cockatrice/cockatrice/).
Adding a new string to translate is as easy as adding the string in the
Adding a new string for translation is as easy as adding the string in the
`tr("")` function, the string will be picked up as translatable automatically
and translated as needed.
For example, setting the text of a label in the way that the string
@@ -315,9 +319,9 @@ nameLabel.setText(tr("My name is:"));
To translate a string that would have plural forms you can add the amount to
the tr() call, also you can add an extra string as a hint for translators:
```c++
QString message = tr("Everyone draws %n cards", "pop up message", amount);
QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
```
See [QT's wiki on translations](
See [Qt's wiki on translations](
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals)
If you're about to propose a change that adds or modifies any translatable
@@ -325,7 +329,7 @@ string in the code, you don't need to take care of adding the new strings to
the translation files.<br>
We have an automated process to update our language source files on a schedule
and provide the translators on Transifex with the new contents.<br>
Maintainers can also manually trigger this on demand.
Maintainers can also manually trigger this workflow on demand via GitHub Actions.
### Maintaining Translations (for maintainers) ###
@@ -389,27 +393,27 @@ Now you are ready to commit your changes and open a PR.
</details>
Once the changes get merged, Transifex will pick up the modified files
automatically (checked every few hours) and update their online editor where
automatically (checked every few hours) and update the web editor where
translators will be able to translate the new strings right in the browser.
### Releasing Translations (for maintainers) ###
Before rushing out a new release, a maintainer should fetch the most up to date
Before publishing a new release, a maintainer should fetch the most up to date
translations from Transifex and commit them into the Cockatrice source code.
This can be done manually from the Transifex web interface, but it's quite time
consuming.
As an alternative, you can install the Transifex CLI:
http://docs.transifex.com/developer/client/
We utilize the official GitHub integration to push all languages that are 100%
translated from Transifex to our GitHub repo automatically.
On top, it runs on a quarterly schedule to update changes for incomplete languages.
A synchronisation/update can also be triggered manually from the Transifex web interface
and a translation treshold can be set.
As an alternative, you can install the [Transifex CLI](https://developers.transifex.com/docs/cli).
You'll then be able to use a git-like cli command to push and pull translations
from Transifex to the source code and vice versa.
### Adding Translations (for translators) ###
As a translator you can help translate the new strings on [Transifex](
https://www.transifex.com/projects/p/cockatrice/).
As a translator, you can help to translate new strings on [Transifex](
https://www.transifex.com/projects/p/cockatrice/) to your native language.
Please have a look at the specific [FAQ for translators](
https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ).
@@ -419,9 +423,9 @@ https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ).
### Publishing A New Release ###
We use [GitHub Releases](https://github.com/Cockatrice/Cockatrice/releases) to
publish new stable versions and betas.
Whenever a git tag is pushed to the repository github will create a draft
release and upload binaries automatically.
publish new stable versions and beta releases.
Whenever a git tag is pushed to the repository, GitHub will create a draft
release and upload binaries from our CI automatically.
To create a tag, simply do the following:
```bash
@@ -433,18 +437,16 @@ git push $UPSTREAM $TAG_NAME
```
You should define the variables as such:
```
`$UPSTREAM` - the remote for git@github.com:Cockatrice/Cockatrice.git
`$TAG_NAME` should be formatted as:
- `YYYY-MM-DD-Release-MAJ.MIN.PATCH` for **stable releases**
- `YYYY-MM-DD-Development-MAJ.MIN.PATCH-beta.X` for **beta releases**<br>
With *MAJ.MIN.PATCH* being the NEXT release version!
```
- `$UPSTREAM`: the remote for `git@github.com:Cockatrice/Cockatrice.git`
- `$TAG_NAME` should be formatted as:
- `YYYY-MM-DD-Release-MAJ.MIN.PATCH` for **stable releases**
- `YYYY-MM-DD-Development-MAJ.MIN.PATCH-beta.X` for **beta releases**<br>
With <kbd>MAJ</kbd>.<kbd>MIN</kbd>.<kbd>PATCH</kbd> being the NEXT release version!
This will cause a tagged release to be established on the GitHub repository,
with the binaries being added to the release whenever they are ready.
The release is initially a draft, where the path notes can be edited and after
all is good and ready it can be published on GitHub.
with the binaries being added to the release whenever they are done building in CI.
The release is initially a draft, where the release notes can be edited and after
all is checked and ready, it can be published as GitHub release.
If you use a SemVer tag including "beta", the release that will be created at
GitHub will be marked as a "Pre-release" automatically.
The target of the `.../latest` URL will not be changed in that case, it always
@@ -457,7 +459,7 @@ revoke the tag by doing the following:
git push --delete upstream $TAG_NAME
git tag -d $TAG_NAME
```
You can also do this on GitHub, you'll also want to delete the new release.
You can also do this on GitHub, you'll also want to delete the false release.
In the first lines of [CMakeLists.txt](
https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt)
@@ -468,25 +470,24 @@ coming from the tag title, it's good practice to increment the ones at CMake
after every full release to stress that master is ahead of the last stable
release.
The preferred flow of operation is:
* Just before a release, make sure the version number in CMakeLists.txt is set
to the same release version you are about to tag.
* This is also the time to change the pretty name in CMakeLists.txt called
GIT_TAG_RELEASENAME and commit and push these changes.
* Tag the release following the previously described syntax in order to get it
correctly built and deployed by CI.
* Wait for the configure step to create the release and update the patch
notes.
* Check on the github actions page for build progress which should be the top
listed [here](
- Just before a release, make sure the version number in CMakeLists.txt is set
to the same release version you are about to tag.
- This is also the time to change the pretty name in CMakeLists.txt called
`GIT_TAG_RELEASENAME` and commit and push these changes.
- Tag the release following the previously described syntax in order to get it
correctly built and deployed by CI.
- Wait for the configuration step to create the release and update the patch
notes.
- Check on the GitHub Actions page for build progress which should be the top
listed [here](
https://github.com/Cockatrice/Cockatrice/actions?query=event%3Apush+-branch%3Amaster
).
* When the build has been completed you can verify all uploaded files on the
release are in order and hit the publish button.
* After the release is complete, update the CMake version number again to the
next targeted beta version, typically increasing `PROJECT_VERSION_PATCH` by
one.
- When the build has been completed, you can verify if all uploaded files on the
draft release are included and hit the publish button.
- After the release is complete, update the CMake version number again to the
next targeted beta version, typically increasing `PROJECT_VERSION_PATCH` by
one.
When releasing a new stable version, all previous beta releases (and tags)
should be deleted. This is needed for Cockatrice to update users of the "Beta"
release channel correctly to the latest stable version as well.
should be deleted as well.
This can be done the same way as revoking tags, mentioned above.

View File

@@ -330,7 +330,7 @@ jobs:
if: matrix.make_package
uses: actions/upload-artifact@v4
with:
name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-dmg
name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-package
path: ${{steps.build.outputs.path}}
if-no-files-found: error

View File

@@ -74,11 +74,11 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
project("Cockatrice" VERSION 2.10.0)
project("Cockatrice" VERSION 2.10.2)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
set(GIT_TAG_RELEASENAME "Rings of the Wild")
set(GIT_TAG_RELEASENAME "Omenpath")
endif()
# Use c++20 for all targets

View File

@@ -26,6 +26,6 @@ RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=
WORKDIR /home/servatrice
EXPOSE 4747 4748
EXPOSE 4748
ENTRYPOINT [ "servatrice", "--log-to-console" ]

View File

@@ -128,9 +128,9 @@ First, create an image from the Dockerfile<br>
`cd /path/to/Cockatrice-Repo/`
`docker build -t servatrice .`<br>
And then run it<br>
`docker run -i -p 4747:4747/tcp -t servatrice:latest`<br>
`docker run -i -p 4748:4748 -t servatrice:latest`<br>
>Note: Running this command exposes the TCP port 4747 of the docker container<br>
>Note: Running this command exposes the port 4748 of the docker container<br>
to permit connections to the server.
Find more information on how to use Servatrice with Docker in our [wiki](https://github.com/Cockatrice/Cockatrice/wiki/Setting-up-Servatrice#using-docker).
@@ -145,7 +145,7 @@ docker-compose build # Build the Servatrice image using the same Dockerfile a
docker-compose up # Setup and run both the MySQL server and Servatrice.
```
>Note: Similar to the above Docker setup, this will expose TCP ports 4747 and 4748.
>Note: Similar to the above Docker setup, this will expose port 4748.
>Note: The first time running the docker-compose setup, the MySQL server will take a little time to run the initial setup scripts. Due to this, the Servatrice instance may fail the first few attempts to connect to the database. Servatrice is set to `restart: always` in the docker-compose.yml, which will allow it to continue attempting to start up. Once the MySQL scripts have completed, Servatrice should then connect automatically on the next attempt.

View File

@@ -4,196 +4,9 @@
project(Cockatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
set(cockatrice_SOURCES
src/game/cards/abstract_card_drag_item.cpp
src/game/cards/abstract_card_item.cpp
src/client/game_logic/abstract_client.cpp
src/game/board/abstract_counter.cpp
src/game/board/abstract_graphics_item.cpp
src/game/board/arrow_item.cpp
src/game/board/arrow_target.cpp
src/game/cards/card_database.cpp
src/game/cards/card_database_manager.cpp
src/game/cards/card_database_model.cpp
src/game/cards/card_database_parser/card_database_parser.cpp
src/game/cards/card_database_parser/cockatrice_xml_3.cpp
src/game/cards/card_database_parser/cockatrice_xml_4.cpp
src/game/cards/card_drag_item.cpp
src/game/filters/filter_card.cpp
src/client/ui/widgets/cards/card_info_frame_widget.cpp
src/client/ui/widgets/cards/card_info_picture_widget.cpp
src/client/ui/widgets/cards/card_info_text_widget.cpp
src/client/ui/widgets/cards/card_info_display_widget.cpp
src/client/ui/widgets/cards/card_size_widget.cpp
src/game/cards/card_item.cpp
src/game/cards/card_list.cpp
src/game/zones/card_zone.cpp
src/server/chat_view/chat_view.cpp
src/game/board/counter_general.cpp
src/deck/custom_line_edit.cpp
src/deck/deck_loader.cpp
src/deck/deck_list_model.cpp
src/deck/deck_stats_interface.cpp
src/dialogs/dlg_connect.cpp
src/dialogs/dlg_convert_deck_to_cod_format.cpp
src/dialogs/dlg_create_token.cpp
src/dialogs/dlg_create_game.cpp
src/dialogs/dlg_edit_avatar.cpp
src/dialogs/dlg_edit_password.cpp
src/dialogs/dlg_edit_tokens.cpp
src/dialogs/dlg_edit_user.cpp
src/dialogs/dlg_filter_games.cpp
src/dialogs/dlg_forgot_password_challenge.cpp
src/dialogs/dlg_forgot_password_request.cpp
src/dialogs/dlg_forgot_password_reset.cpp
src/dialogs/dlg_load_deck_from_clipboard.cpp
src/dialogs/dlg_load_remote_deck.cpp
src/dialogs/dlg_manage_sets.cpp
src/dialogs/dlg_move_top_cards_until.cpp
src/dialogs/dlg_register.cpp
src/dialogs/dlg_roll_dice.cpp
src/dialogs/dlg_settings.cpp
src/dialogs/dlg_tip_of_the_day.cpp
src/dialogs/dlg_update.cpp
src/dialogs/dlg_view_log.cpp
src/dialogs/dlg_load_deck.cpp
src/game/deckview/deck_view.cpp
src/game/deckview/deck_view_container.cpp
src/game/filters/filter_string.cpp
src/game/filters/filter_builder.cpp
src/game/filters/filter_tree.cpp
src/game/filters/filter_tree_model.cpp
src/client/ui/layouts/flow_layout.cpp
src/client/ui/widgets/general/layout_containers/flow_widget.cpp
src/game/game_scene.cpp
src/game/game_selector.cpp
src/game/games_model.cpp
src/game/game_view.cpp
src/client/get_text_with_max.cpp
src/game/hand_counter.cpp
src/server/handle_public_servers.cpp
src/game/zones/hand_zone.cpp
src/client/game_logic/key_signals.cpp
src/client/ui/line_edit_completer.cpp
src/server/local_client.cpp
src/server/local_server.cpp
src/server/local_server_interface.cpp
src/utility/logger.cpp
src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
src/client/ui/widgets/general/display/banner_widget.cpp
src/client/ui/widgets/general/display/labeled_input.cpp
src/client/ui/widgets/general/display/dynamic_font_size_label.cpp
src/client/ui/widgets/general/display/dynamic_font_size_push_button.cpp
src/client/ui/widgets/general/display/shadow_background_label.cpp
src/main.cpp
src/server/message_log_widget.cpp
src/client/ui/layouts/overlap_layout.cpp
src/client/ui/widgets/general/layout_containers/overlap_widget.cpp
src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp
src/server/pending_command.cpp
src/game/phase.cpp
src/client/ui/phases_toolbar.cpp
src/client/ui/picture_loader/picture_loader.cpp
src/client/ui/picture_loader/picture_loader_worker.cpp
src/client/ui/picture_loader/picture_to_load.cpp
src/game/zones/pile_zone.cpp
src/client/ui/pixel_map_generator.cpp
src/game/player/player.cpp
src/game/player/player_list_widget.cpp
src/game/player/player_target.cpp
src/client/ui/widgets/printing_selector/all_zones_card_amount_widget.cpp
src/client/ui/widgets/printing_selector/card_amount_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector.cpp
src/client/ui/widgets/printing_selector/printing_selector_card_display_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_card_search_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_card_selection_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_view_options_toolbar_widget.cpp
src/client/ui/widgets/printing_selector/printing_selector_view_options_widget.cpp
src/client/ui/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
src/client/network/release_channel.cpp
src/client/network/client_update_checker.cpp
src/server/remote/remote_client.cpp
src/server/remote/remote_decklist_tree_widget.cpp
src/server/remote/remote_replay_list_tree_widget.cpp
src/client/network/replay_timeline_widget.cpp
src/game/zones/select_zone.cpp
src/utility/sequence_edit.cpp
src/client/network/sets_model.cpp
src/settings/card_database_settings.cpp
src/settings/download_settings.cpp
src/settings/game_filters_settings.cpp
src/settings/layouts_settings.cpp
src/settings/message_settings.cpp
src/settings/recents_settings.cpp
src/settings/servers_settings.cpp
src/settings/settings_manager.cpp
src/settings/cache_settings.cpp
src/settings/shortcuts_settings.cpp
src/settings/shortcut_treeview.cpp
src/settings/card_override_settings.cpp
src/settings/debug_settings.cpp
src/client/sound_engine.cpp
src/client/network/spoiler_background_updater.cpp
src/game/zones/stack_zone.cpp
src/client/tabs/tab.cpp
src/client/tabs/tab_account.cpp
src/client/tabs/tab_admin.cpp
src/client/tabs/tab_deck_editor.cpp
src/client/tabs/tab_deck_storage.cpp
src/client/tabs/tab_game.cpp
src/client/tabs/tab_logs.cpp
src/client/tabs/tab_message.cpp
src/client/tabs/tab_replays.cpp
src/client/tabs/tab_room.cpp
src/client/tabs/tab_server.cpp
src/client/tabs/tab_supervisor.cpp
src/client/tabs/api/edhrec/tab_edhrec.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp
src/game/zones/table_zone.cpp
src/client/tapped_out_interface.cpp
src/client/ui/theme_manager.cpp
src/client/ui/tip_of_the_day.cpp
src/client/translate_counter_name.cpp
src/client/update_downloader.cpp
src/server/user/user_context_menu.cpp
src/server/user/user_info_connection.cpp
src/server/user/user_info_box.cpp
src/server/user/user_list_manager.cpp
src/server/user/user_list_widget.cpp
src/client/ui/window_main.cpp
src/game/zones/view_zone_widget.cpp
src/game/zones/view_zone.cpp
src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
${VERSION_STRING_CPP}
)
file(GLOB_RECURSE cockatrice_CPP_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/cockatrice/src/*.cpp)
set(cockatrice_SOURCES ${cockatrice_CPP_FILES} ${VERSION_STRING_CPP})
add_subdirectory(sounds)
add_subdirectory(themes)
@@ -338,6 +151,13 @@ if(APPLE)
PATTERN "tls/*.dylib"
)
install(
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
DESTINATION ${qtconf_dest_dir}/
FILES_MATCHING
PATTERN "*.ini"
)
install(
CODE "
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths]
@@ -372,6 +192,13 @@ if(WIN32)
PATTERN "*.dll"
)
install(
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
DESTINATION ./
FILES_MATCHING
PATTERN "*.ini"
)
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"

View File

@@ -1,5 +1,5 @@
<RCC>
<qresource prefix="/" >
<qresource prefix="/">
<file>resources/cardback.svg</file>
<file>resources/cockatrice.svg</file>
<file>resources/hand.svg</file>
@@ -13,9 +13,12 @@
<file>resources/icons/arrow_top_green.svg</file>
<file>resources/icons/arrow_up_green.svg</file>
<file>resources/icons/clearsearch.svg</file>
<file>resources/icons/cogwheel.svg</file>
<file>resources/icons/conceded.svg</file>
<file>resources/icons/decrement.svg</file>
<file>resources/icons/delete.svg</file>
<file>resources/icons/dropdown_collapsed.svg</file>
<file>resources/icons/dropdown_expanded.svg</file>
<file>resources/icons/forgot_password.svg</file>
<file>resources/icons/increment.svg</file>
<file>resources/icons/info.svg</file>
@@ -24,7 +27,9 @@
<file>resources/icons/pencil.svg</file>
<file>resources/icons/player.svg</file>
<file>resources/icons/ready_start.svg</file>
<file>resources/icons/reload.svg</file>
<file>resources/icons/remove_row.svg</file>
<file>resources/icons/rename.svg</file>
<file>resources/icons/scales.svg</file>
<file>resources/icons/search.svg</file>
<file>resources/icons/settings.svg</file>
@@ -35,6 +40,12 @@
<file>resources/icons/update.png</file>
<file>resources/icons/view.svg</file>
<file>resources/icons/mana/B.svg</file>
<file>resources/icons/mana/G.svg</file>
<file>resources/icons/mana/R.svg</file>
<file>resources/icons/mana/U.svg</file>
<file>resources/icons/mana/W.svg</file>
<file>resources/config/general.svg</file>
<file>resources/config/appearance.svg</file>
<file>resources/config/interface.svg</file>
@@ -342,19 +353,29 @@
<!-- ADD TIP OF THE DAY IMAGES HERE -->
<file>resources/tips/images/accounts_tab.png</file>
<file>resources/tips/images/add_card.png</file>
<file>resources/tips/images/arrows.png</file>
<file>resources/tips/images/card_select.png</file>
<file>resources/tips/images/cockatrice_register.png</file>
<file>resources/tips/images/cockatrice_wiki.png</file>
<file>resources/tips/images/coin_flip.png</file>
<file>resources/tips/images/counter_expression.png</file>
<file>resources/tips/images/discord.png</file>
<file>resources/tips/images/edhrec.png</file>
<file>resources/tips/images/expand_card_view.png</file>
<file>resources/tips/images/face_down.png</file>
<file>resources/tips/images/filter_games.png</file>
<file>resources/tips/images/github_logo.png</file>
<file>resources/tips/images/highlight_cards.png</file>
<file>resources/tips/images/pawns.png</file>
<file>resources/tips/images/setpt.png</file>
<file>resources/tips/images/shortcuts.png</file>
<file>resources/tips/images/syntax_help.png</file>
<file>resources/tips/images/themes.png</file>
<file>resources/tips/images/tip_of_the_day.png</file>
<file>resources/tips/images/token.png</file>
<file>resources/tips/images/updates.png</file>
<file>resources/tips/images/visual_deck_tags.png</file>
<file>resources/tips/tips_of_the_day.xml</file>
<file>resources/help/search.md</file>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,9 @@
[Rules]
# Uncomment a rule to disable logging for that category
# The default log level is info
*.debug = false
# Uncomment a rule to disable logging for that category,
# or set .debug = true for that category to see debug level logs
# main = false
# qt_translator = false
@@ -44,11 +48,15 @@
# cockatrice_xml.* = false
# cockatrice_xml.xml_3_parser = false
# cockatrice_xml.xml_4_parser = false
# card_info = false
# card_list = false
# flow_layout = false
flow_widget.* = false
#flow_layout = false
#flow_widget = false
#flow_widget.size = false
# card_info_picture_widget = false
# pixel_map_generator = false
# filter_string = false
# filter_string = false

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="102.5024mm" height="102.24421mm"
viewBox="0 0 102.5024 102.24421" version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)" sodipodi:docname="cog_wheel.svg"
xmlns="http://www.w3.org/2000/svg">
<sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25"
inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="0.66101291"
inkscape:cx="146.74449" inkscape:cy="193.64221" inkscape:window-width="1829"
inkscape:window-height="951" inkscape:window-x="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:current-layer="layer1"/>
<defs id="defs1"/>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-66.148059,-97.377896)">
<g id="g1" transform="translate(-165.09777,-82.009607)">
<g id="g2" transform="matrix(0.26458333,0,0,0.26458333,64.983323,161.37858)">
<path style="fill:#333333;stroke-width:94.4882"
d="m 721.34301,94.413051 40.00966,29.763289 29.51933,-9.27053 14.63768,-46.840589 36.83817,0.48792 13.41787,46.108699 28.29952,9.5145 40.74155,-28.299519 29.0314,21.468599 -15.85748,47.08454 16.58937,23.66426 48.30432,-2.68358 12.93,34.88648 -41.71741,27.07971 -0.48792,29.27536 40.49763,26.10387 -10.2464,34.15459 -48.54834,-0.97584 -18.78502,23.42029 15.85749,45.37681 -28.29952,21.71256 -41.22947,-29.27536 -28.54348,9.75846 -13.66184,47.57246 -38.30194,-0.73188 -14.63768,-48.30435 -27.07971,-9.27054 -40.00966,29.27537 -30.25121,-20.49276 17.32125,-46.35266 -17.80918,-24.64009 -49.76812,3.17149 -11.22222,-34.64251 40.74155,-27.07971 0.48792,-30.00725 -41.71739,-27.32367 12.19807,-32.93479 48.30435,2.68358 20.24879,-24.6401 -17.32126,-47.08455 z"
id="path1"/>
<circle style="fill:#f9f9f9;stroke-width:72.1317" id="path2" cx="822.09906" cy="261.28262"
r="83.434792"/>
<circle style="fill:#4d4d4d;stroke-width:40.5147" id="circle2" cx="822.09906" cy="261.28262"
r="46.863361"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="460.5" height="531.74"
viewBox="0 0 460.5 531.74" overflow="visible" enable-background="new 0 0 460.5 531.74" xml:space="preserve">
<polygon fill="#918d8d" points="0.5,0.866 459.5,265.87 0.5,530.874 "/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="531.74" height="460.5"
viewBox="0 0 531.74 460.5" overflow="visible" enable-background="new 0 0 531.74 460.5" xml:space="preserve">
<polygon fill="#918d8d" points="530.874,0.5 265.87,459.5 0.866,0.5 "/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="169.34801mm"
height="169.34801mm"
viewBox="0 0 600 600"
version="1.1"
id="svg1"
sodipodi:docname="B.svg"
inkscape:export-filename="B.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1"/>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm">
<inkscape:page
x="0"
y="0"
width="600"
height="600"
id="page2"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<path
id="Selection"
fill="none"
stroke="#000000"
stroke-width="1"
d="m 275,0.21 c 0,0 42,0 42,0 0,0 19,1.51 19,1.51 29.13,3.58 56.99,10.83 84,22.43 33.64,14.45 59.89,32.91 86.91,57.41 17.38,15.76 34.67,37.56 47.14,57.44 22.78,36.31 37.1,74.56 43.22,117 1.28,8.88 2.72,19.08 2.73,28 0,0 0,34 0,34 0,0 -0.91,10 -0.91,10 -2.36,32.29 -11.93,67.59 -25.4,97 -27.01,58.98 -70.49,105.42 -126.69,137.57 -34.3,19.63 -79.56,33.64 -119,36.52 0,0 -12,0.91 -12,0.91 0,0 -34,0 -34,0 0,0 -10,-0.91 -10,-0.91 -26.57,-1.94 -53.09,-8.27 -78,-17.67 C 163.49,569.9 134.12,553.46 109,532.57 79.87,508.33 54.41,478.39 36.31,445 -5.08,368.65 -11.86,276.57 18.05,195 47.85,113.75 113.21,48.92 194,18.42 213.55,11.04 236.32,5.43 257,2.43 Z"
style="display:inline;fill:#ababab;fill-opacity:1"/>
<path
d="m 299.67889,84.945431 c -118.26949,0 -214.122988,83.892719 -214.122988,187.357619 0,58.63289 30.863828,110.90901 79.041488,145.28579 8.02961,5.77129 12.71356,15.13917 11.29165,25.0089 l -7.86234,55.37086 c -1.17098,8.02962 5.01851,15.22281 13.13176,15.22281 h 64.98968 v -46.83939 c 0,-3.68025 3.01111,-6.69135 6.69135,-6.69135 h 13.38268 c 3.68024,0 6.69135,3.0111 6.69135,6.69135 v 46.83939 h 53.53075 v -46.83939 c 0,-3.68025 3.01111,-6.69135 6.69134,-6.69135 h 13.38269 c 3.68023,0 6.69134,3.0111 6.69134,6.69135 v 46.83939 h 64.98967 c 8.11327,0 14.30275,-7.19319 13.13177,-15.22281 l -7.86234,-55.37086 c -1.4219,-9.78609 3.1784,-19.23761 11.29165,-25.0089 48.17767,-34.37678 79.0415,-86.6529 79.0415,-145.28579 0,-103.4649 -95.8535,-187.357619 -214.123,-187.357619 z M 219.38277,352.59917 c -29.52555,0 -53.53075,-24.0052 -53.53075,-53.53075 0,-29.52555 24.0052,-53.53075 53.53075,-53.53075 29.52555,0 53.53075,24.0052 53.53075,53.53075 0,29.52555 -24.0052,53.53075 -53.53075,53.53075 z m 160.59225,0 c -29.52555,0 -53.53075,-24.0052 -53.53075,-53.53075 0,-29.52555 24.0052,-53.53075 53.53075,-53.53075 29.52555,0 53.53075,24.0052 53.53075,53.53075 0,29.52555 -24.0052,53.53075 -53.53075,53.53075 z"
id="path1"
style="fill:#4d4d4d;fill-opacity:1;stroke:#000000;stroke-width:0.836419;stroke-opacity:1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="169.34801mm"
height="169.34801mm"
viewBox="0 0 600 600"
version="1.1"
id="svg1"
sodipodi:docname="B.svg"
inkscape:export-filename="U.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1"/>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm">
<inkscape:page
x="0"
y="0"
width="600"
height="600"
id="page2"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<path
id="Selection"
fill="none"
stroke="#000000"
stroke-width="1"
d="m 275,0.21 c 0,0 42,0 42,0 0,0 19,1.51 19,1.51 29.13,3.58 56.99,10.83 84,22.43 33.64,14.45 59.89,32.91 86.91,57.41 17.38,15.76 34.67,37.56 47.14,57.44 22.78,36.31 37.1,74.56 43.22,117 1.28,8.88 2.72,19.08 2.73,28 0,0 0,34 0,34 0,0 -0.91,10 -0.91,10 -2.36,32.29 -11.93,67.59 -25.4,97 -27.01,58.98 -70.49,105.42 -126.69,137.57 -34.3,19.63 -79.56,33.64 -119,36.52 0,0 -12,0.91 -12,0.91 0,0 -34,0 -34,0 0,0 -10,-0.91 -10,-0.91 -26.57,-1.94 -53.09,-8.27 -78,-17.67 C 163.49,569.9 134.12,553.46 109,532.57 79.87,508.33 54.41,478.39 36.31,445 -5.08,368.65 -11.86,276.57 18.05,195 47.85,113.75 113.21,48.92 194,18.42 213.55,11.04 236.32,5.43 257,2.43 Z"
style="display:inline;fill:#5f9f56;fill-opacity:1"/>
<path
d="m 488.52401,428.04591 -80.36527,-83.50228 h 30.81218 c 9.06362,0 17.08104,-4.61393 20.90363,-12.05153 3.82262,-7.41912 2.56521,-15.92714 -3.29949,-22.19282 l -78.59482,-83.87142 h 29.06188 c 9.15413,0 17.40291,-4.93688 20.98411,-12.55902 3.54092,-7.50219 1.87106,-16.23169 -4.26525,-22.22053 L 312.83499,83.240199 c -6.06588,-5.951919 -17.38282,-5.951919 -23.45873,0 L 178.44019,191.64831 c -6.13629,5.98884 -7.79612,14.71834 -4.26523,22.22053 3.58119,7.62214 11.84001,12.55902 20.99414,12.55902 h 29.06189 l -78.60489,83.88987 c -5.84454,6.25645 -7.10199,14.75525 -3.28944,22.18361 3.82262,7.42836 11.82996,12.04229 20.90363,12.04229 h 30.81222 l -80.36527,83.50228 c -6.03569,6.26567 -7.40379,14.84754 -3.58117,22.38663 3.7723,7.42839 12.07138,12.22682 21.13497,12.22682 h 129.63669 c 0,0 -36.12127,66.38546 0,62.75824 h 80.45578 c 33.50708,5.78651 0,-62.75824 0,-62.75824 h 129.63668 c 9.06361,0 17.36266,-4.79843 21.13499,-12.22682 3.82262,-7.53909 2.45452,-16.12096 -3.58117,-22.38663 z"
id="ENVIR"
style="fill:#3f6e3a;fill-opacity:1;stroke:#000000;stroke-width:1.09833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="169.34801mm"
height="169.34801mm"
viewBox="0 0 600 600"
version="1.1"
id="svg1"
sodipodi:docname="B.svg"
inkscape:export-filename="B.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1"/>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm">
<inkscape:page
x="0"
y="0"
width="600"
height="600"
id="page2"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<path
id="Selection"
fill="none"
stroke="#000000"
stroke-width="1"
d="m 275,0.21 c 0,0 42,0 42,0 0,0 19,1.51 19,1.51 29.13,3.58 56.99,10.83 84,22.43 33.64,14.45 59.89,32.91 86.91,57.41 17.38,15.76 34.67,37.56 47.14,57.44 22.78,36.31 37.1,74.56 43.22,117 1.28,8.88 2.72,19.08 2.73,28 0,0 0,34 0,34 0,0 -0.91,10 -0.91,10 -2.36,32.29 -11.93,67.59 -25.4,97 -27.01,58.98 -70.49,105.42 -126.69,137.57 -34.3,19.63 -79.56,33.64 -119,36.52 0,0 -12,0.91 -12,0.91 0,0 -34,0 -34,0 0,0 -10,-0.91 -10,-0.91 -26.57,-1.94 -53.09,-8.27 -78,-17.67 C 163.49,569.9 134.12,553.46 109,532.57 79.87,508.33 54.41,478.39 36.31,445 -5.08,368.65 -11.86,276.57 18.05,195 47.85,113.75 113.21,48.92 194,18.42 213.55,11.04 236.32,5.43 257,2.43 Z"
style="display:inline;fill:#c95e46;fill-opacity:1"/>
<path
style="display:inline;fill:#7e3525;fill-opacity:1;stroke:#000000;stroke-width:0.999129;stroke-dasharray:none;stroke-opacity:1"
d="m 278.25473,537.27772 c -22.36635,-1.29637 -48.78648,-8.31223 -69.74195,-18.51997 -47.30139,-23.04123 -75.19872,-59.72004 -85.45343,-112.35234 -2.77041,-14.21911 -2.51753,-48.32296 0.4755,-64.1246 4.84373,-25.57244 17.23939,-55.03329 30.65191,-72.85059 3.74877,-4.97991 16.33133,-18.11906 30.23479,-31.57223 13.09764,-12.67346 26.64745,-26.67202 30.11069,-31.10792 16.66894,-21.35045 26.46602,-44.71272 29.68313,-70.78286 1.93489,-15.67951 0.37962,-45.867231 -3.5802,-69.491071 -0.58906,-3.51428 0.36584,-3.52724 11.47726,-0.15583 36.51454,11.0792 67.37028,34.791071 89.32423,68.643431 9.96686,15.36862 20.07787,39.31263 24.78919,58.70364 2.23257,9.18886 2.53252,44.73878 0.46396,54.98571 -0.75708,3.75024 -1.10086,7.09425 -0.76396,7.43114 1.5021,1.5021 14.63666,-6.65479 21.98002,-13.65016 10.59844,-10.09621 14.32693,-17.61354 15.94714,-32.15254 l 1.16788,-10.47988 3.4626,3.95724 c 26.80954,30.6392 41.75268,56.11528 54.11775,92.26358 11.02075,32.21825 14.93902,62.79297 12.47422,97.33768 -1.88493,26.41774 -10.16486,50.6958 -24.95555,73.17369 -6.31625,9.59901 -30.86306,34.05172 -40.40914,40.25423 -23.44452,15.23295 -50.15355,25.25695 -77.26806,28.99903 -12.48732,1.72336 -38.04339,2.42637 -54.18798,1.49062 z m 48.34948,-30.34196 c 30.78066,-8.0491 53.53846,-30.12825 59.69792,-57.9177 2.48916,-11.23027 2.26087,-34.25269 -0.43747,-44.11787 -5.3998,-19.74181 -9.72931,-26.61651 -30.6237,-48.62658 -16.92925,-17.8332 -21.73398,-24.64464 -25.45673,-36.0889 -2.38622,-7.33554 -2.63126,-9.53814 -2.57644,-23.15876 0.0334,-8.27872 0.486,-17.4607 1.00593,-20.40441 0.75887,-4.29636 0.66928,-5.45812 -0.45409,-5.88919 -2.12997,-0.81736 -16.25071,4.45522 -21.90917,8.1807 -25.69349,16.91641 -42.62753,52.30879 -40.22917,84.07948 0.39046,5.17228 0.2831,8.79277 -0.26071,8.79277 -1.78935,0 -13.49435,-13.46378 -15.34792,-17.65406 -1.03171,-2.33236 -2.15967,-6.42651 -2.50657,-9.09812 -1.00143,-7.71233 -1.60778,-8.14619 -5.46339,-3.90915 -13.31687,14.63425 -24.58113,35.69359 -29.53283,55.21379 -11.75615,46.34414 2.14731,84.38956 37.55813,102.77397 16.56338,8.59931 29.2427,11.25369 51.26679,10.73261 12.58064,-0.29766 17.41119,-0.85366 25.26942,-2.90858 z"
id="path8"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="169.34801mm"
height="169.34801mm"
viewBox="0 0 600 600"
version="1.1"
id="svg1"
sodipodi:docname="U.svg"
inkscape:export-filename="U.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1"/>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm">
<inkscape:page
x="0"
y="0"
width="600"
height="600"
id="page2"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<path
id="Selection"
fill="none"
stroke="#000000"
stroke-width="1"
d="m 275,0.21 c 0,0 42,0 42,0 0,0 19,1.51 19,1.51 29.13,3.58 56.99,10.83 84,22.43 33.64,14.45 59.89,32.91 86.91,57.41 17.38,15.76 34.67,37.56 47.14,57.44 22.78,36.31 37.1,74.56 43.22,117 1.28,8.88 2.72,19.08 2.73,28 0,0 0,34 0,34 0,0 -0.91,10 -0.91,10 -2.36,32.29 -11.93,67.59 -25.4,97 -27.01,58.98 -70.49,105.42 -126.69,137.57 -34.3,19.63 -79.56,33.64 -119,36.52 0,0 -12,0.91 -12,0.91 0,0 -34,0 -34,0 0,0 -10,-0.91 -10,-0.91 -26.57,-1.94 -53.09,-8.27 -78,-17.67 C 163.49,569.9 134.12,553.46 109,532.57 79.87,508.33 54.41,478.39 36.31,445 -5.08,368.65 -11.86,276.57 18.05,195 47.85,113.75 113.21,48.92 194,18.42 213.55,11.04 236.32,5.43 257,2.43 Z"
style="display:inline;fill:#4a8fd8;fill-opacity:1"/>
<path
style="display:inline;fill:#3266af;fill-opacity:1;stroke:#000000;stroke-width:1.06525;stroke-opacity:1"
d="m 275.20608,536.5693 c -58.69596,-9.5447 -107.01545,-50.69962 -125.66779,-107.0344 -9.25532,-27.95342 -10.39669,-58.82111 -3.22092,-87.1088 5.31307,-20.94475 9.06296,-28.00552 81.87785,-154.17 38.54798,-66.79109 70.40461,-121.77997 70.79251,-122.19751 1.00535,-1.08215 140.3595,240.63696 145.26785,251.97682 8.6735,20.03855 12.6923,39.82747 12.6923,62.49805 0,44.25387 -16.70161,83.24077 -48.85852,114.0514 -21.46938,20.57055 -45.69209,33.39124 -76.46586,40.47213 -12.83139,2.95245 -42.64639,3.75165 -56.41742,1.51231 z"
id="path2"
sodipodi:nodetypes="ssssssssss"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="169.34801mm"
height="169.34801mm"
viewBox="0 0 600 600"
version="1.1"
id="svg1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="W.svg"
inkscape:export-filename="W.png"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs1"/>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm">
<inkscape:page
x="0"
y="0"
width="600"
height="600"
id="page2"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<path
id="Selection"
fill="none"
stroke="#000000"
stroke-width="1"
d="m 275,0.21 c 0,0 42,0 42,0 0,0 19,1.51 19,1.51 29.13,3.58 56.99,10.83 84,22.43 33.64,14.45 59.89,32.91 86.91,57.41 17.38,15.76 34.67,37.56 47.14,57.44 22.78,36.31 37.1,74.56 43.22,117 1.28,8.88 2.72,19.08 2.73,28 0,0 0,34 0,34 0,0 -0.91,10 -0.91,10 -2.36,32.29 -11.93,67.59 -25.4,97 -27.01,58.98 -70.49,105.42 -126.69,137.57 -34.3,19.63 -79.56,33.64 -119,36.52 0,0 -12,0.91 -12,0.91 0,0 -34,0 -34,0 0,0 -10,-0.91 -10,-0.91 -26.57,-1.94 -53.09,-8.27 -78,-17.67 C 163.49,569.9 134.12,553.46 109,532.57 79.87,508.33 54.41,478.39 36.31,445 -5.08,368.65 -11.86,276.57 18.05,195 47.85,113.75 113.21,48.92 194,18.42 213.55,11.04 236.32,5.43 257,2.43 Z"
style="display:inline;fill:#ffefb3;fill-opacity:1"/>
<path
id="circle8"
d="M 387.28106,302.711 A 85.962512,85.962512 0 0 1 301.31852,388.67353 85.962512,85.962512 0 0 1 215.35597,302.711 85.962512,85.962512 0 0 1 301.31852,216.74844 85.962512,85.962512 0 0 1 387.28106,302.711 Z M 191.75349,358.6114 143.05798,460.97151 245.4181,412.27601 A 121.73882,121.73882 0 0 1 191.75349,358.6114 Z M 178.33753,302.711 c 0,-13.16763 2.23687,-25.83857 5.96271,-38.01232 L 77.71657,302.711 184.30024,340.7233 a 119.25435,119.25435 0 0 1 -5.96271,-38.0123 z m 178.8814,109.56501 102.36011,48.6955 -48.6955,-102.36011 a 121.73882,121.73882 0 0 1 -53.66461,53.66461 z m -55.90041,13.41595 c -13.16762,0 -26.0869,-2.23687 -38.01232,-5.9627 l 38.01232,106.58368 38.01231,-106.58368 a 119.25435,119.25435 0 0 1 -38.01231,5.9627 z m 109.56502,-178.88139 48.6955,-102.36012 -102.36011,48.69551 a 122.98106,122.98106 0 0 1 53.66461,53.66461 z M 245.66643,193.14596 143.30658,144.45045 192.00209,246.81057 a 121.73882,121.73882 0 0 1 53.66434,-53.66461 z M 524.92046,302.711 418.33679,264.69868 a 119.25435,119.25435 0 0 1 0,76.02462 z M 301.31852,179.73 c 13.16761,0 26.0869,2.23688 38.01231,5.96271 L 301.31852,79.109036 263.3062,185.69271 A 119.25435,119.25435 0 0 1 301.31852,179.73 Z"
style="fill:#b4a35e;fill-opacity:1;stroke:#000000;stroke-width:1.09833;stroke-dasharray:none;stroke-opacity:1"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M586.9,373.6l95.6-84.4c-49.6-39.2-129.4-79.1-198.2-79.1c-134.9,0-248.2,90.5-280.2,212.8L10.6,374.5C64.4,168.3,255.2,15.8,482.5,15.8c126.7,0,258.8,63.7,345.4,141.4l90.8-80.1L990,479.6L586.9,373.6z M317.5,710.8c49.6,39.2,129.4,79.1,198.2,79.1c134.9,0,248.2-90.5,280.2-212.8l193.5,48.5c-53.7,206.2-244.6,358.7-471.9,358.7c-126.7,0-258.8-63.7-345.4-141.4l-90.8,80.1L10,520.4l403.1,106L317.5,710.8z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path transform="scale(50)" d="m17 16h4v-8h-4" fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2"/>
<path transform="scale(50)" d="m12 8h-9v8h9" fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2"/>
<path transform="scale(50)" d="m6 12h5" fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2"/>
<path transform="scale(50)" d="m20 4h-2c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h2" fill="none" stroke="#000"
stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
<path transform="scale(50)" d="m12 20h2c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2h-2" fill="none" stroke="#000"
stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -1,99 +1,222 @@
<tips>
<tip>
<title>Tip of the Day</title>
<text>Tip of the Day is a new feature to Cockatrice that allows users to get information about the newest features of the program and some of the most commonly asked questions!</text>
<image>tip_of_the_day.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Suggesting New Tips</title>
<text>You can suggest new Tips of the Day by reaching out to the development team on &lt;a href="https://discord.gg/3Z9yzmA"&gt;Discord&lt;/a&gt;!</text>
<image>discord.png</image>
<date>2023-10-18</date>
</tip>
<tip>
<title>Reporting Bugs</title>
<text>If you encounter a bug while using Cockatrice, you can report the bug to the development team via &lt;a href="https://github.com/cockatrice/cockatrice/issues"&gt;GitHub&lt;a&gt;</text>
<image>github_logo.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>FAQ/Troubleshooting Wiki</title>
<text>You can find answers to the most common questions and some helpful Cockatrice toubleshooting over on the &lt;a href="https://github.com/cockatrice/cockatrice/wiki"&gt;GitHub wiki&lt;a&gt;</text>
<image>cockatrice_wiki.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Register for a Server</title>
<text>Click on either Cockatrice (Windows) or Actions (Mac) and then Register to server... When the dialogue appears, fill out the desired server information.</text>
<image>cockatrice_register.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Drawing Arrows</title>
<text>You can draw arrows of different color by holding a combination of keys!
Right Click: Red Arrow
Shift + Right Click: Green Arrow
Alt + Right Click: Blue Arrow
Cmd + Right Click: Yellow Arrow
</text>
<image>arrows.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Filtering Games</title>
<text>Don't see all the active games? Want to see a smaller selection? Use the Game Filters to change your horizon</text>
<image>filter_games.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Upload Custom Avatar</title>
<text>Want to show off your hippo avatar? Need to update your password? Check out the Accounts Tab for more info!</text>
<image>accounts_tab.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Common Shortcuts</title>
<text>You can find a full list of shortcuts &lt;a href="https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"&gt;on the wiki&lt;/a&gt;, but a short list:
&lt;br&gt;Roll a die: CTRL + I
&lt;br&gt;Mulligan: CTRL + M
&lt;br&gt;Draw a card: CTRL + D
&lt;br&gt;Undo a draw: CTRL + SHIFT + D
&lt;br&gt;View Sideboard: CTRL + F3
&lt;br&gt;Change Life: CTRL + L
&lt;br&gt;All shortcuts can be customized via Settings->Shortcuts!
</text>
<image>shortcuts.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Changing Themes</title>
<text>Did you know Cockatrice has custom themes? You can either &lt;a href="https://github.com/Cockatrice/Cockatrice/wiki/Themes"&gt;create one yourself&lt;/a&gt; or use one of the several pre-loaded ones! Go to Settings->Appearance and try them out!</text>
<image>themes.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Flip of the Coin</title>
<text>You can flip a coin instead of rolling a die by rolling a 2 sided die instead!</text>
<image>coin_flip.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Face Down Cards</title>
<text>You can hold Shift while dragging or clicking on a card to have it enter play face down</text>
<image>face_down.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Counter expressions</title>
<text>When setting a counter value, you can type a math expression in the box and the counter will be set to the result.&lt;br&gt;The "x" variable contains the current counter value.</text>
<image>counter_expression.png</image>
<date>2019-02-02</date>
</tip>
<tip>
<title>Power and Toughness</title>
<text>You can add and subtract to a creature's stats.&lt;br&gt;With a card selected, set the power and toughness ( default: ctrl + p ) and enter +3/-1 to increase power by three while decreasing toughness by one.&lt;br&gt;You can also reset it to the original value ( default: ctrl + alt + 0 ).</text>
<image>setpt.png</image>
<date>2019-03-02</date>
</tip>
<tip>
<title>Tip of the Day</title>
<text>Tip of the Day is a feature to Cockatrice that allows users to get information about the newest features of the program and some of the most commonly asked questions!
Check back in with major updates for new tips to be added or old tips to be updated as features are added or expanded upon!
</text>
<image>tip_of_the_day.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>Join the Community</title>
<text>You can join the community to find games, interact with other players, suggest new 'Tips of the Day' and provide user feedback to the development or support teams on &lt;a href="https://discord.gg/3Z9yzmA"&gt;Discord&lt;/a&gt;!</text>
<image>discord.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Reporting Bugs and Requesting Features</title>
<text>If you encounter a bug while using Cockatrice, you can report the bug to and request the feature from the development team via &lt;a href="https://github.com/cockatrice/cockatrice/issues"&gt;GitHub&lt;a&gt;</text>
<image>github_logo.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>FAQ/Troubleshooting Wiki</title>
<text>You can find answers to the most common questions and some helpful Cockatrice troubleshooting over on the &lt;a href="https://github.com/cockatrice/cockatrice/wiki"&gt;GitHub wiki&lt;a&gt;</text>
<image>cockatrice_wiki.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Register for a Server</title>
<text>Click on either Cockatrice (Windows) or Actions (Mac) and then Register to server... When the dialogue appears, fill out the desired server information.</text>
<image>cockatrice_register.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Filtering Games</title>
<text>Don't see an open game or want to see a smaller selection? Use the Game Filters to change your horizon!</text>
<image>filter_games.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Common Shortcuts</title>
<text>You can find a full list of default shortcuts &lt;a href="https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"&gt;on the wiki&lt;/a&gt; or in Settings -> Shortcuts, but a short list:
&lt;ul&gt;
&lt;li&gt;Roll a die: CMD/CTRL + I&lt;/li&gt;
&lt;li&gt;Mulligan: CMD/CTRL + M&lt;/li&gt;
&lt;li&gt;Draw 1 / X card(s): CMD/CTRL + D / E&lt;/li&gt;
&lt;li&gt;Undo a draw: CMD/CTRL + SHIFT + D&lt;/li&gt;
&lt;li&gt;View Library / Sideboard: F3 / CMD/CTRL + F3&lt;/li&gt;
&lt;li&gt;Change Life: CMD/CTRL + L&lt;/li&gt;
&lt;li&gt;Show Card Info: Middle Mouse Click&lt;/li&gt;
&lt;/ul&gt;
All shortcuts can be customized via Cockatrice -> Settings -> Shortcuts!
</text>
<image>shortcuts.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Power and Toughness</title>
<text>You can add and subtract to a creature's stats.
With a card selected, with the following keybindings:
&lt;ul&gt;
&lt;li&gt;Set P/T to any value: CTRL + P&lt;/li&gt;
&lt;li&gt;+1 or -1 to both to P/T: CMD/CTRL + ALT + '+' or '-'&lt;/li&gt;
&lt;li&gt;+1 or -1 to Power Only: CMD/CTRL + '+' or '-'&lt;/li&gt;
&lt;li&gt;+1 or -1 to Toughness Only: ALT + '+' or '-'&lt;/li&gt;
&lt;li&gt;Set P/T to Default: CMD/CTRL + ALT + 0&lt;/li&gt;
&lt;/ul&gt;
All shortcuts can be customized via Cockatrice -> Settings -> Shortcuts!
</text>
<image>setpt.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Drawing Arrows</title>
<text>You can draw arrows of different color by holding a combination of keys!
&lt;ul&gt;
&lt;li&gt;Right Click: Red Arrow&lt;/li&gt;
&lt;li&gt;SHIFT + Right Click: Green Arrow&lt;/li&gt;
&lt;li&gt;ALT + Right Click: Blue Arrow&lt;/li&gt;
&lt;li&gt;CMD/CTRL + Right Click: Yellow Arrow&lt;/li&gt;
&lt;/ul&gt;
</text>
<image>arrows.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>General Etiquette</title>
<text>When playing on Cockatrice there are general "rules of engagement" for a better play experience for all players.
The following are some of the expectations a player should reasonably expect for how to operate smooth and enjoyable games:
&lt;ul&gt;
&lt;li&gt;Have a "Rule 0" conversation pre-game with your fellow players on the expectations of the game including deck strength, house rules (e.g. no take backs and "may is not must"), etc.&lt;/li&gt;
&lt;li&gt;When rolling your die for turn order, it is often best to create a token with the text of your roll (using CTRL + T and typing the number into "Name") and then tap this token when created to show that you have resolved your mulligans and are ready to start the game.&lt;/li&gt;
&lt;li&gt;Use the phases and steps trackers on the left of the client to show others where in the turn you are and announce when moving between them. (Double Click for the action of this button)&lt;/li&gt;
&lt;li&gt;When your turn is over leave it on your end step and DO NOT HIT PASS. Allow other players to respond if they can, and then if not, let the next player pass to themselves.&lt;/li&gt;
&lt;li&gt;Please treat others kindly and respect as per &lt;a href="https://cockatrice.us/terms.php"&gt;our Terms of Service&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</text>
<image>token.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>Flip of the Coin</title>
<text>You can flip a coin instead of rolling a die by rolling a 2 sided die instead!</text>
<image>coin_flip.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Upload Custom Avatar</title>
<text>Want to show off your hippo avatar? Need to update your password? Check out the Accounts Tab for more info!</text>
<image>accounts_tab.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Player Icon Key</title>
<text>Besides your name is a player icon, this is a key for what they mean:
&lt;ul&gt;
&lt;li&gt;Flag: Country of Origin Player Selected&lt;/li&gt;
&lt;li&gt;Purple Heart: Donator (&lt;a href="https://cockatrice.us/donate"&gt;Help support us and donate here!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Gold Star: VIP (Special Admin Given Role)&lt;/li&gt;
&lt;li&gt;Brown Gavel (Hammer): Judge / Rule's Lawyer&lt;/li&gt;
&lt;li&gt;Black/White Pawn: Moderator (Support for Client Issues)&lt;/li&gt;
&lt;li&gt;Red: Administrator (Sever Operators)&lt;/li&gt;
&lt;/ul&gt;
</text>
<image>pawns.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Changing Themes</title>
<text>Did you know Cockatrice has custom themes? You can either &lt;a href="https://github.com/Cockatrice/Cockatrice/wiki/Themes"&gt;create one yourself&lt;/a&gt; or use one of the several preloaded ones! Go to Settings->Appearance and try them out!</text>
<image>themes.png</image>
<date>2018-03-01</date>
</tip>
<tip>
<title>Face Down Cards</title>
<text>You can hold Shift while dragging or clicking on a card to have it enter play face down.
You can also hold CTRL + SHIFT and click and drag from your library to move bottom card face down as well!
</text>
<image>face_down.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Counter expressions</title>
<text>When setting a counter value, you can type a math expression in the box and the counter will be set to the result.&lt;br&gt;The "x" variable contains the current counter value.</text>
<image>counter_expression.png</image>
<date>2019-02-02</date>
</tip>
<tip>
<title>Select Multiple Cards</title>
<text>You can click and drag in any zone in order to highlight all cards within the created box.
You can also hold CMD/CTRL and clik or click and drag to maintain other previously selected cards while adding others!
You can move, alter and attach multiple highlighted cards at the same time!
Other useful multi-select keybindings:
&lt;ul&gt;
&lt;li&gt;Select All Cards in Zone: CMD/CTRL + A&lt;/li&gt;
&lt;li&gt;Select All Cards in Column: CMD/CTRL + SHIFT + C&lt;/li&gt;
&lt;li&gt;Select All Cards in Row: CMD/CTRL + SHIFT + X&lt;/li&gt;
&lt;/ul&gt;
</text>
<image>highlight_cards.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Auto Cascade and Search Parameters</title>
<text>You can use the default keybind of CMD/CTRL + SHIFT + Y in order to perform auto cascade or similar effects from your library using Scryfall search syntax.
If you are unfamiliar with the syntax you may find it by opening a deck in deck editor and clicking the "i" next to the search bar in order to pull up a list of syntax commands.
This same syntax can be used in the deck editor search bar as well to help you find the best cards for your decks!
</text>
<image>syntax_help.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Visual Deck Storage and Tags</title>
<text>You can now view your saved decks visually with integrated folder,tags and filter support!
Pro Tip: If you want to use emoji's press WinKey + '.' or CTRL + CMD + SPACE on Windows or Mac respectively!
</text>
<image>visual_deck_tags.png</image>
<date>2025-02-09</date>
</tip>
<tip>
<title>Card Selector And Card Preferences</title>
<text>Cockatrice's deck editor now has the ability for players to bling out their decks by selecting the arts for cards on a per-card basis!
You can also have multiple printings of the same card in your deck if you so choose!
Not only that, other players will see the arts you have chosen by default!
If you want to disable this feature for other players: Settings -> Appearance -> Select "Override all card art with personal set preference"
</text>
<image>card_select.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>EDHREC Integration</title>
<text>We now have in-client integration for EDHREC, which allows you to right-click on any card name in the deck editor in order to bring up the contextual menu for the card.</text>
<image>edhrec.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>Add Cards to Deck</title>
<text>You can now right-click anywhere on a card in any card info view in order to add it to any decks you have open in your tabs.</text>
<image>add_card.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>Update Client and Card Database</title>
<text>You can update your client by going to Help and selecting whichever you wish to update.
Client Update: Updates the client (if available) for new features, fixes and changes.
Card Update: Updates card sources for spoilers and new card printings.
Updating your card sources can often fix issues of cards not working or displaying properly, if not, go to Cockatrice -> Settings -> Card Sources -> Update Spoilers
</text>
<image>updates.png</image>
<date>2025-02-10</date>
</tip>
<tip>
<title>Expand Card View Window</title>
<text>Double click on the title bar of the card view window to expand it! Double click again to shrink it back down.
You can configure the initial and expanded heights of the window in the settings, under "Appearance".
</text>
<image>expand_card_view.png</image>
<date>2025-02-27</date>
</tip>
</tips>

View File

@@ -0,0 +1,201 @@
#include "deck_editor_menu.h"
#include "../../../settings/cache_settings.h"
#include "../../../settings/shortcuts_settings.h"
DeckEditorMenu::DeckEditorMenu(QWidget *parent, AbstractTabDeckEditor *_deckEditor)
: QMenu(parent), deckEditor(_deckEditor)
{
aNewDeck = new QAction(QString(), this);
connect(aNewDeck, SIGNAL(triggered()), deckEditor, SLOT(actNewDeck()));
aLoadDeck = new QAction(QString(), this);
connect(aLoadDeck, SIGNAL(triggered()), deckEditor, SLOT(actLoadDeck()));
loadRecentDeckMenu = new QMenu(this);
connect(&SettingsCache::instance().recents(), &RecentsSettings::recentlyOpenedDeckPathsChanged, this,
&DeckEditorMenu::updateRecentlyOpened);
aClearRecents = new QAction(QString(), this);
connect(aClearRecents, &QAction::triggered, this, &DeckEditorMenu::actClearRecents);
updateRecentlyOpened();
aSaveDeck = new QAction(QString(), this);
connect(aSaveDeck, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeck()));
aSaveDeckAs = new QAction(QString(), this);
connect(aSaveDeckAs, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckAs()));
aLoadDeckFromClipboard = new QAction(QString(), this);
connect(aLoadDeckFromClipboard, SIGNAL(triggered()), deckEditor, SLOT(actLoadDeckFromClipboard()));
aEditDeckInClipboard = new QAction(QString(), this);
connect(aEditDeckInClipboard, SIGNAL(triggered()), deckEditor, SLOT(actEditDeckInClipboard()));
aEditDeckInClipboardRaw = new QAction(QString(), this);
connect(aEditDeckInClipboardRaw, SIGNAL(triggered()), deckEditor, SLOT(actEditDeckInClipboardRaw()));
aSaveDeckToClipboard = new QAction(QString(), this);
connect(aSaveDeckToClipboard, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboard()));
aSaveDeckToClipboardNoSetInfo = new QAction(QString(), this);
connect(aSaveDeckToClipboardNoSetInfo, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboardNoSetInfo()));
aSaveDeckToClipboardRaw = new QAction(QString(), this);
connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboardRaw()));
aSaveDeckToClipboardRawNoSetInfo = new QAction(QString(), this);
connect(aSaveDeckToClipboardRawNoSetInfo, SIGNAL(triggered()), deckEditor,
SLOT(actSaveDeckToClipboardRawNoSetInfo()));
aPrintDeck = new QAction(QString(), this);
connect(aPrintDeck, SIGNAL(triggered()), deckEditor, SLOT(actPrintDeck()));
aExportDeckDecklist = new QAction(QString(), this);
connect(aExportDeckDecklist, SIGNAL(triggered()), deckEditor, SLOT(actExportDeckDecklist()));
aExportDeckDecklistXyz = new QAction(QString(), this);
connect(aExportDeckDecklistXyz, SIGNAL(triggered()), deckEditor, SLOT(actExportDeckDecklistXyz()));
aAnalyzeDeckDeckstats = new QAction(QString(), this);
connect(aAnalyzeDeckDeckstats, SIGNAL(triggered()), deckEditor, SLOT(actAnalyzeDeckDeckstats()));
aAnalyzeDeckTappedout = new QAction(QString(), this);
connect(aAnalyzeDeckTappedout, SIGNAL(triggered()), deckEditor, SLOT(actAnalyzeDeckTappedout()));
analyzeDeckMenu = new QMenu(this);
analyzeDeckMenu->addAction(aExportDeckDecklist);
analyzeDeckMenu->addAction(aExportDeckDecklistXyz);
analyzeDeckMenu->addSeparator();
analyzeDeckMenu->addAction(aAnalyzeDeckDeckstats);
analyzeDeckMenu->addAction(aAnalyzeDeckTappedout);
aClose = new QAction(QString(), this);
connect(aClose, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::closeRequest);
editDeckInClipboardMenu = new QMenu(this);
editDeckInClipboardMenu->addAction(aEditDeckInClipboard);
editDeckInClipboardMenu->addAction(aEditDeckInClipboardRaw);
saveDeckToClipboardMenu = new QMenu(this);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardNoSetInfo);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRawNoSetInfo);
addAction(aNewDeck);
addAction(aLoadDeck);
addMenu(loadRecentDeckMenu);
addAction(aSaveDeck);
addAction(aSaveDeckAs);
addSeparator();
addAction(aLoadDeckFromClipboard);
addMenu(editDeckInClipboardMenu);
addMenu(saveDeckToClipboardMenu);
addSeparator();
addAction(aPrintDeck);
addMenu(analyzeDeckMenu);
addSeparator();
addAction(deckEditor->filterDockWidget->aClearFilterOne);
addAction(deckEditor->filterDockWidget->aClearFilterAll);
addSeparator();
addAction(aClose);
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
}
void DeckEditorMenu::setSaveStatus(bool newStatus)
{
aSaveDeck->setEnabled(newStatus);
aSaveDeckAs->setEnabled(newStatus);
aSaveDeckToClipboard->setEnabled(newStatus);
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
aSaveDeckToClipboardRaw->setEnabled(newStatus);
aSaveDeckToClipboardRawNoSetInfo->setEnabled(newStatus);
saveDeckToClipboardMenu->setEnabled(newStatus);
aPrintDeck->setEnabled(newStatus);
analyzeDeckMenu->setEnabled(newStatus);
}
void DeckEditorMenu::updateRecentlyOpened()
{
loadRecentDeckMenu->clear();
for (const auto &deckPath : SettingsCache::instance().recents().getRecentlyOpenedDeckPaths()) {
QAction *aRecentlyOpenedDeck = new QAction(deckPath, this);
loadRecentDeckMenu->addAction(aRecentlyOpenedDeck);
connect(aRecentlyOpenedDeck, &QAction::triggered, deckEditor,
[=, this] { deckEditor->actOpenRecent(aRecentlyOpenedDeck->text()); });
}
loadRecentDeckMenu->addSeparator();
loadRecentDeckMenu->addAction(aClearRecents);
aClearRecents->setEnabled(SettingsCache::instance().recents().getRecentlyOpenedDeckPaths().length() > 0);
}
void DeckEditorMenu::actClearRecents()
{
SettingsCache::instance().recents().clearRecentlyOpenedDeckPaths();
}
void DeckEditorMenu::retranslateUi()
{
setTitle(tr("&Deck Editor"));
aNewDeck->setText(tr("&New deck"));
aLoadDeck->setText(tr("&Load deck..."));
loadRecentDeckMenu->setTitle(tr("Load recent deck..."));
aClearRecents->setText(tr("Clear"));
aSaveDeck->setText(tr("&Save deck"));
aSaveDeckAs->setText(tr("Save deck &as..."));
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
editDeckInClipboardMenu->setTitle(tr("Edit deck in clipboard"));
aEditDeckInClipboard->setText(tr("Annotated"));
aEditDeckInClipboardRaw->setText(tr("Not Annotated"));
saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard"));
aSaveDeckToClipboard->setText(tr("Annotated"));
aSaveDeckToClipboardNoSetInfo->setText(tr("Annotated (No set info)"));
aSaveDeckToClipboardRaw->setText(tr("Not Annotated"));
aSaveDeckToClipboardRawNoSetInfo->setText(tr("Not Annotated (No set info)"));
aPrintDeck->setText(tr("&Print deck..."));
analyzeDeckMenu->setTitle(tr("&Send deck to online service"));
aExportDeckDecklist->setText(tr("Create decklist (decklist.org)"));
aExportDeckDecklistXyz->setText(tr("Create decklist (decklist.xyz)"));
aAnalyzeDeckDeckstats->setText(tr("Analyze deck (deckstats.net)"));
aAnalyzeDeckTappedout->setText(tr("Analyze deck (tappedout.net)"));
aClose->setText(tr("&Close"));
}
void DeckEditorMenu::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
aNewDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aNewDeck"));
aLoadDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeck"));
aSaveDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeck"));
aSaveDeckAs->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckAs"));
aLoadDeckFromClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromClipboard"));
aEditDeckInClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboard"));
aEditDeckInClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboardRaw"));
aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck"));
aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist"));
aExportDeckDecklistXyz->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklistXyz"));
aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck"));
aAnalyzeDeckTappedout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeckTappedout"));
aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
aSaveDeckToClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboard"));
aSaveDeckToClipboardNoSetInfo->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardNoSetInfo"));
aSaveDeckToClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardRaw"));
aSaveDeckToClipboardRawNoSetInfo->setShortcuts(
shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardRawNoSetInfo"));
aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
}

View File

@@ -0,0 +1,32 @@
#ifndef DECK_EDITOR_MENU_H
#define DECK_EDITOR_MENU_H
#include "../../tabs/abstract_tab_deck_editor.h"
#include <QMenu>
class AbstractTabDeckEditor;
class DeckEditorMenu : public QMenu
{
Q_OBJECT
public:
explicit DeckEditorMenu(QWidget *parent, AbstractTabDeckEditor *deckEditor);
AbstractTabDeckEditor *deckEditor;
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aExportDeckDecklist,
*aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
void setSaveStatus(bool newStatus);
public slots:
void updateRecentlyOpened();
void actClearRecents();
void retranslateUi();
void refreshShortcuts();
};
#endif

View File

@@ -11,6 +11,11 @@
#include <QSysInfo>
#include <QtGlobal>
#if defined(Q_OS_MACOS)
#include <sys/sysctl.h>
#include <sys/types.h>
#endif
#define STABLERELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases/latest"
#define STABLEMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
#define STABLETAG_URL "https://api.github.com/repos/Cockatrice/Cockatrice/git/refs/tags/"
@@ -33,31 +38,42 @@ ReleaseChannel::~ReleaseChannel()
void ReleaseChannel::checkForUpdates()
{
QString releaseChannelUrl = getReleaseChannelUrl();
qCDebug(ReleaseChannelLog) << "Searching for updates on the channel: " << releaseChannelUrl;
qCInfo(ReleaseChannelLog) << "Searching for updates on the channel: " << releaseChannelUrl;
response = netMan->get(QNetworkRequest(releaseChannelUrl));
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
}
// Different release channel checking functions for different operating systems
#if defined(Q_OS_MACOS)
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
{
static QRegularExpression version_regex("macOS-(\\d+)\\.(\\d+)");
#if defined(Q_OS_MACOS)
static QRegularExpression version_regex("macOS(\\d+)");
auto match = version_regex.match(fileName);
if (!match.hasMatch()) {
return false;
}
auto getSystemVersion = [] {
// QSysInfo does not go through translation layers
// We need to use sysctl to reliably detect the underlying architecture
char arch[255];
size_t len = sizeof(arch);
if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) {
// Intel mac is only supported on macOS 13 versions
if (QString::fromUtf8(arch).contains("Intel")) {
return 13;
}
}
return QSysInfo::productVersion().split(".")[0].toInt();
};
// older(smaller) releases are compatible with a newer or the same system version
int sys_maj = QSysInfo::productVersion().split(".")[0].toInt();
int sys_min = QSysInfo::productVersion().split(".")[1].toInt();
int sys_maj = getSystemVersion();
int rel_maj = match.captured(1).toInt();
int rel_min = match.captured(2).toInt();
return rel_maj < sys_maj || (rel_maj == sys_maj && rel_min <= sys_min);
}
return rel_maj == sys_maj;
#elif defined(Q_OS_WIN)
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
{
#if Q_PROCESSOR_WORDSIZE == 4
return fileName.contains("32bit");
#elif Q_PROCESSOR_WORDSIZE == 8
@@ -68,16 +84,15 @@ bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
return fileName.contains("Win10");
}
#else
Q_UNUSED(fileName);
return false;
#endif
#else // If the OS doesn't fit one of the above #defines, then it will never match
Q_UNUSED(fileName);
return false;
#endif
}
#else
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &)
{
// If the OS doesn't fit one of the above #defines, then it will never match
return false;
}
#endif
QString StableReleaseChannel::getManualDownloadUrl() const
{
@@ -101,7 +116,7 @@ void StableReleaseChannel::releaseListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "No reply received from the release update server.";
qCWarning(ReleaseChannelLog) << "No reply received from the release update server.";
emit error(tr("No reply received from the release update server."));
return;
}
@@ -109,7 +124,7 @@ void StableReleaseChannel::releaseListFinished()
QVariantMap resultMap = jsonResponse.toVariant().toMap();
if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
resultMap.contains("published_at"))) {
qWarning() << "Invalid received from the release update server:" << resultMap;
qCWarning(ReleaseChannelLog) << "Invalid received from the release update server:" << resultMap;
emit error(tr("Invalid reply received from the release update server."));
return;
}
@@ -123,37 +138,29 @@ void StableReleaseChannel::releaseListFinished()
if (resultMap.contains("assets")) {
auto rawAssets = resultMap["assets"].toList();
// [(name, url)]
QVector<std::pair<QString, QString>> assets;
std::transform(rawAssets.begin(), rawAssets.end(), std::back_inserter(assets), [](QVariant _asset) {
QVariantMap asset = _asset.toMap();
for (const auto &rawAsset : rawAssets) {
QVariantMap asset = rawAsset.toMap();
QString name = asset["name"].toString();
QString url = asset["browser_download_url"].toString();
return std::make_pair(name, url);
});
auto _releaseAsset = std::find_if(assets.begin(), assets.end(), [](std::pair<QString, QString> nameAndUrl) {
return downloadMatchesCurrentOS(nameAndUrl.first);
});
if (_releaseAsset != assets.end()) {
std::pair<QString, QString> releaseAsset = *_releaseAsset;
auto releaseUrl = releaseAsset.second;
lastRelease->setDownloadUrl(releaseUrl);
if (downloadMatchesCurrentOS(name)) {
lastRelease->setDownloadUrl(url);
break;
}
}
}
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
qCDebug(ReleaseChannelLog) << "Got reply from release server, name=" << lastRelease->getName()
<< "desc=" << lastRelease->getDescriptionUrl()
<< "date=" << lastRelease->getPublishDate() << "url=" << lastRelease->getDownloadUrl();
qCInfo(ReleaseChannelLog) << "Got reply from release server, name=" << lastRelease->getName()
<< "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
<< "url=" << lastRelease->getDownloadUrl();
const QString &tagName = resultMap["tag_name"].toString();
QString url = QString(STABLETAG_URL) + tagName;
qCDebug(ReleaseChannelLog) << "Searching for commit hash corresponding to stable channel tag: " << tagName;
qCInfo(ReleaseChannelLog) << "Searching for commit hash corresponding to stable channel tag: " << tagName;
response = netMan->get(QNetworkRequest(url));
connect(response, &QNetworkReply::finished, this, &StableReleaseChannel::tagListFinished);
}
@@ -165,24 +172,24 @@ void StableReleaseChannel::tagListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "No reply received from the tag update server.";
qCWarning(ReleaseChannelLog) << "No reply received from the tag update server.";
emit error(tr("No reply received from the tag update server."));
return;
}
QVariantMap resultMap = jsonResponse.toVariant().toMap();
if (!(resultMap.contains("object") && resultMap["object"].toMap().contains("sha"))) {
qWarning() << "Invalid received from the tag update server.";
qCWarning(ReleaseChannelLog) << "Invalid received from the tag update server.";
emit error(tr("Invalid reply received from the tag update server."));
return;
}
lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
qCDebug(ReleaseChannelLog) << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
qCInfo(ReleaseChannelLog) << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
@@ -225,7 +232,7 @@ void BetaReleaseChannel::releaseListFinished()
QVariantMap resultMap = array.at(0).toObject().toVariantMap();
if (array.empty() || resultMap.empty()) {
qWarning() << "No reply received from the release update server:" << QString(jsonData);
qCWarning(ReleaseChannelLog) << "No reply received from the release update server:" << QString(jsonData);
emit error(tr("No reply received from the release update server."));
return;
}
@@ -234,7 +241,7 @@ void BetaReleaseChannel::releaseListFinished()
if (!resultMap.contains("assets") || !resultMap.contains("author") || !resultMap.contains("tag_name") ||
!resultMap.contains("target_commitish") || !resultMap.contains("assets_url") ||
!resultMap.contains("published_at")) {
qWarning() << "Invalid received from the release update server:" << resultMap;
qCWarning(ReleaseChannelLog) << "Invalid received from the release update server:" << resultMap;
emit error(tr("Invalid reply received from the release update server."));
return;
}
@@ -249,13 +256,13 @@ void BetaReleaseChannel::releaseListFinished()
lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
qCDebug(ReleaseChannelLog) << "Got reply from release server, size=" << resultMap.size()
<< "name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl()
<< "commit=" << lastRelease->getCommitHash() << "date=" << lastRelease->getPublishDate();
qCInfo(ReleaseChannelLog) << "Got reply from release server, size=" << resultMap.size()
<< "name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl()
<< "commit=" << lastRelease->getCommitHash() << "date=" << lastRelease->getPublishDate();
QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
qCDebug(ReleaseChannelLog) << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
qCInfo(ReleaseChannelLog) << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
connect(response, &QNetworkReply::finished, this, &BetaReleaseChannel::fileListFinished);
}
@@ -267,7 +274,7 @@ void BetaReleaseChannel::fileListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
qWarning() << "No reply received from the file update server.";
qCWarning(ReleaseChannelLog) << "No reply received from the file update server.";
emit error(tr("No reply received from the file update server."));
return;
}
@@ -275,7 +282,7 @@ void BetaReleaseChannel::fileListFinished()
QVariantList resultList = jsonResponse.toVariant().toList();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
bool compatibleVersion = false;
@@ -292,7 +299,7 @@ void BetaReleaseChannel::fileListFinished()
if (downloadMatchesCurrentOS(*url)) {
compatibleVersion = true;
lastRelease->setDownloadUrl(*url);
qCDebug(ReleaseChannelLog) << "Found compatible version url=" << *url;
qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url;
break;
}
}

View File

@@ -28,7 +28,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(
// File exists means we're in spoiler season
startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
} else {
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
}
}
@@ -67,7 +67,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
reply->deleteLater();
emit spoilerCheckerDone();
} else {
qCDebug(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -81,11 +81,11 @@ bool SpoilerBackgroundUpdater::deleteSpoilerFile()
// Delete the spoiler.xml file
if (file.exists() && file.remove()) {
qCDebug(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
return true;
}
qCDebug(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
return false;
}
@@ -101,24 +101,24 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
}
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
emit spoilerCheckerDone();
} else if (errorCode == QNetworkReply::NoError) {
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
startSpoilerDownloadProcess(SPOILERS_URL, true);
} else if (errorCode == QNetworkReply::HostNotFoundError) {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
}
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
emit spoilerCheckerDone();
} else {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
}
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -139,19 +139,19 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
}
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
return false;
}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
file.close();
return false;
}
if (file.write(data) == -1) {
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
file.close();
return false;
}
@@ -159,7 +159,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
file.close();
// Data written, so reload the card database
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
// If the user has notifications enabled, let them know
@@ -202,12 +202,12 @@ QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
qCDebug(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
file.close();
return hash.result();
} else {
qCDebug(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
file.close();
return QByteArray();
}
@@ -221,7 +221,7 @@ QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
qCDebug(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
return hash.result();
}

View File

@@ -37,7 +37,7 @@ SoundEngine::~SoundEngine()
void SoundEngine::soundEnabledChanged()
{
if (SettingsCache::instance().getSoundEnabled()) {
qCDebug(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
if (!player) {
player = new QMediaPlayer;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
@@ -46,7 +46,7 @@ void SoundEngine::soundEnabledChanged()
#endif
}
} else {
qCDebug(SoundEngineLog) << "SoundEngine: disabling sound";
qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
if (player) {
player->stop();
player->deleteLater();
@@ -90,7 +90,7 @@ void SoundEngine::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
qCDebug(SoundEngineLog) << "Sounds theme name not set, setting default value";
qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
}
}
@@ -131,7 +131,7 @@ QStringMap &SoundEngine::getAvailableThemes()
void SoundEngine::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getSoundThemeName();
qCDebug(SoundEngineLog) << "Sound theme changed:" << themeName;
qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
QDir dir = getAvailableThemes().value(themeName);

View File

@@ -0,0 +1,569 @@
#include "abstract_tab_deck_editor.h"
#include "../../client/game_logic/abstract_client.h"
#include "../../client/tapped_out_interface.h"
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
#include "../../deck/deck_stats_interface.h"
#include "../../dialogs/dlg_load_deck.h"
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_database_model.h"
#include "../../server/pending_command.h"
#include "../../settings/cache_settings.h"
#include "../ui/picture_loader/picture_loader.h"
#include "../ui/pixel_map_generator.h"
#include "pb/command_deck_upload.pb.h"
#include "pb/response.pb.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileDialog>
#include <QHeaderView>
#include <QLineEdit>
#include <QMenuBar>
#include <QMessageBox>
#include <QPrintPreviewDialog>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QRegularExpression>
#include <QSplitter>
#include <QTextStream>
#include <QTreeView>
#include <QUrl>
AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks);
databaseDisplayDockWidget = new DeckEditorDatabaseDisplayWidget(this);
deckDockWidget = new DeckEditorDeckDockWidget(this);
cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this);
filterDockWidget = new DeckEditorFilterDockWidget(this);
printingSelectorDockWidget = new DeckEditorPrintingSelectorDockWidget(this);
connect(deckDockWidget, &DeckEditorDeckDockWidget::deckChanged, this, &AbstractTabDeckEditor::onDeckChanged);
connect(deckDockWidget, &DeckEditorDeckDockWidget::cardChanged, this, &AbstractTabDeckEditor::updateCard);
connect(this, &AbstractTabDeckEditor::decrementCard, deckDockWidget, &DeckEditorDeckDockWidget::actDecrementCard);
connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, this,
&AbstractTabDeckEditor::updateCard);
connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this,
&AbstractTabDeckEditor::actAddCard);
connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this,
&AbstractTabDeckEditor::actAddCardToSideboard);
connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, this,
&AbstractTabDeckEditor::actDecrementCard);
connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, this,
&AbstractTabDeckEditor::actDecrementCardFromSideboard);
connect(filterDockWidget, &DeckEditorFilterDockWidget::clearAllDatabaseFilters, databaseDisplayDockWidget,
&DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters);
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
}
void AbstractTabDeckEditor::updateCard(CardInfoPtr _card)
{
cardInfoDockWidget->updateCard(_card);
printingSelectorDockWidget->printingSelector->setCard(_card, DECK_ZONE_MAIN);
}
void AbstractTabDeckEditor::onDeckChanged()
{
setModified(!isBlankNewDeck());
deckMenu->setSaveStatus(!isBlankNewDeck());
}
void AbstractTabDeckEditor::addCardHelper(const CardInfoPtr info, QString zoneName)
{
if (!info)
return;
if (info->getIsToken())
zoneName = DECK_ZONE_TOKENS;
QModelIndex newCardIndex = deckDockWidget->deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
// recursiveExpand(newCardIndex);
deckDockWidget->deckView->clearSelection();
deckDockWidget->deckView->setCurrentIndex(newCardIndex);
setModified(true);
databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length());
}
void AbstractTabDeckEditor::actAddCard(CardInfoPtr info)
{
if (QApplication::keyboardModifiers() & Qt::ControlModifier)
actAddCardToSideboard(info);
else
addCardHelper(info, DECK_ZONE_MAIN);
deckMenu->setSaveStatus(true);
}
void AbstractTabDeckEditor::actAddCardToSideboard(CardInfoPtr info)
{
addCardHelper(info, DECK_ZONE_SIDE);
deckMenu->setSaveStatus(true);
}
void AbstractTabDeckEditor::actDecrementCard(CardInfoPtr info)
{
emit decrementCard(info, DECK_ZONE_MAIN);
}
void AbstractTabDeckEditor::actDecrementCardFromSideboard(CardInfoPtr info)
{
emit decrementCard(info, DECK_ZONE_SIDE);
}
void AbstractTabDeckEditor::actSwapCard(CardInfoPtr info, QString zoneName)
{
QString providerId = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("uuid");
QString collectorNumber = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("num");
deckDockWidget->swapCard(
deckDockWidget->deckModel->findCard(info->getName(), zoneName, providerId, collectorNumber));
}
/**
* Opens the deck in this tab.
* @param deck The deck. Takes ownership of the object
*/
void AbstractTabDeckEditor::openDeck(DeckLoader *deck)
{
setDeck(deck);
if (!deck->getLastFileName().isEmpty()) {
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(deck->getLastFileName());
}
}
/**
* Sets the currently active deck for this tab
* @param _deck The deck. Takes ownership of the object
*/
void AbstractTabDeckEditor::setDeck(DeckLoader *_deck)
{
deckDockWidget->setDeck(_deck);
PictureLoader::cacheCardPixmaps(
CardDatabaseManager::getInstance()->getCardsByNameAndProviderId(getDeckList()->getCardListWithProviderId()));
setModified(false);
// If they load a deck, make the deck list appear
aDeckDockVisible->setChecked(true);
deckDockWidget->setVisible(aDeckDockVisible->isChecked());
}
DeckLoader *AbstractTabDeckEditor::getDeckList() const
{
return deckDockWidget->getDeckList();
}
void AbstractTabDeckEditor::setModified(bool _modified)
{
modified = _modified;
emit tabTextChanged(this, getTabText());
}
/**
* @brief Returns true if this tab is a blank newly opened tab, as if it was just created with the `New Deck` action.
*/
bool AbstractTabDeckEditor::isBlankNewDeck() const
{
DeckLoader *deck = getDeckList();
return !modified && deck->hasNotBeenLoaded();
}
void AbstractTabDeckEditor::actNewDeck()
{
auto deckOpenLocation = confirmOpen(false);
if (deckOpenLocation == CANCELLED) {
return;
}
if (deckOpenLocation == NEW_TAB) {
emit openDeckEditor(nullptr);
return;
}
cleanDeckAndResetModified();
}
void AbstractTabDeckEditor::cleanDeckAndResetModified()
{
deckMenu->setSaveStatus(false);
deckDockWidget->cleanDeck();
setModified(false);
}
/**
* @brief Displays the save confirmation dialogue that is shown before loading a deck, if required. Takes into
* account the `openDeckInNewTab` settting.
*
* @param openInSameTabIfBlank Open the deck in the same tab instead of a new tab if the current tab is completely
* blank. Only relevant when the `openDeckInNewTab` setting is enabled.
*
* @returns An enum that indicates if and where to load the deck
*/
AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank)
{
// handle `openDeckInNewTab` setting
if (SettingsCache::instance().getOpenDeckInNewTab()) {
if (openInSameTabIfBlank && isBlankNewDeck()) {
return SAME_TAB;
} else {
return NEW_TAB;
}
}
// early return if deck is unmodified
if (!modified) {
return SAME_TAB;
}
// do the save confirmation dialogue
tabSupervisor->setCurrentWidget(this);
QMessageBox *msgBox = createSaveConfirmationWindow();
QPushButton *newTabButton = msgBox->addButton(tr("Open in new tab"), QMessageBox::ApplyRole);
int ret = msgBox->exec();
// `exec()` returns an opaque value if a non-standard button was clicked.
// Directly check if newTabButton was clicked before switching over the standard buttons.
if (msgBox->clickedButton() == newTabButton) {
return NEW_TAB;
}
switch (ret) {
case QMessageBox::Save:
return actSaveDeck() ? SAME_TAB : CANCELLED;
case QMessageBox::Discard:
return SAME_TAB;
default:
return CANCELLED;
}
}
/**
* @brief Creates the base save confirmation dialogue box.
*
* @returns A QMessageBox that can be further modified
*/
QMessageBox *AbstractTabDeckEditor::createSaveConfirmationWindow()
{
QMessageBox *msgBox = new QMessageBox(this);
msgBox->setIcon(QMessageBox::Warning);
msgBox->setWindowTitle(tr("Are you sure?"));
msgBox->setText(tr("The decklist has been modified.\nDo you want to save the changes?"));
msgBox->setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
return msgBox;
}
void AbstractTabDeckEditor::actLoadDeck()
{
auto deckOpenLocation = confirmOpen();
if (deckOpenLocation == CANCELLED) {
return;
}
DlgLoadDeck dialog(this);
if (!dialog.exec())
return;
QString fileName = dialog.selectedFiles().at(0);
openDeckFromFile(fileName, deckOpenLocation);
deckDockWidget->updateBannerCardComboBox();
}
void AbstractTabDeckEditor::actOpenRecent(const QString &fileName)
{
auto deckOpenLocation = confirmOpen();
if (deckOpenLocation == CANCELLED) {
return;
}
openDeckFromFile(fileName, deckOpenLocation);
}
/**
* Actually opens the deck from file
* @param fileName The path of the deck to open
* @param deckOpenLocation Which tab to open the deck
*/
void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
{
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
auto *l = new DeckLoader;
if (l->loadFromFile(fileName, fmt, true)) {
if (deckOpenLocation == NEW_TAB) {
emit openDeckEditor(l);
l->deleteLater();
} else {
deckMenu->setSaveStatus(false);
openDeck(l);
}
} else {
l->deleteLater();
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(fileName));
}
deckMenu->setSaveStatus(true);
}
bool AbstractTabDeckEditor::actSaveDeck()
{
DeckLoader *const deck = getDeckList();
if (deck->getLastRemoteDeckId() != -1) {
QString deckString = deck->writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck"));
return false;
}
Command_DeckUpload cmd;
cmd.set_deck_id(static_cast<google::protobuf::uint32>(deck->getLastRemoteDeckId()));
cmd.set_deck_list(deckString.toStdString());
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(saveDeckRemoteFinished(Response)));
tabSupervisor->getClient()->sendCommand(pend);
return true;
} else if (deck->getLastFileName().isEmpty())
return actSaveDeckAs();
else if (deck->saveToFile(deck->getLastFileName(), deck->getLastFileFormat())) {
setModified(false);
return true;
}
QMessageBox::critical(
this, tr("Error"),
tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
return false;
}
bool AbstractTabDeckEditor::actSaveDeckAs()
{
QFileDialog dialog(this, tr("Save deck"));
dialog.setDirectory(SettingsCache::instance().getDeckPath());
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setDefaultSuffix("cod");
dialog.setNameFilters(DeckLoader::FILE_NAME_FILTERS);
dialog.selectFile(getDeckList()->getName().trimmed());
if (!dialog.exec())
return false;
QString fileName = dialog.selectedFiles().at(0);
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
if (!getDeckList()->saveToFile(fileName, fmt)) {
QMessageBox::critical(
this, tr("Error"),
tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
return false;
}
setModified(false);
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
return true;
}
void AbstractTabDeckEditor::saveDeckRemoteFinished(const Response &response)
{
if (response.response_code() != Response::RespOk)
QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved."));
else
setModified(false);
}
void AbstractTabDeckEditor::actLoadDeckFromClipboard()
{
auto deckOpenLocation = confirmOpen();
if (deckOpenLocation == CANCELLED) {
return;
}
DlgLoadDeckFromClipboard dlg(this);
if (!dlg.exec())
return;
if (deckOpenLocation == NEW_TAB) {
emit openDeckEditor(dlg.getDeckList());
} else {
setDeck(dlg.getDeckList());
setModified(true);
}
deckMenu->setSaveStatus(true);
}
void AbstractTabDeckEditor::editDeckInClipboard(bool annotated)
{
DlgEditDeckInClipboard dlg(*getDeckList(), annotated, this);
if (!dlg.exec())
return;
setDeck(dlg.getDeckList());
setModified(true);
deckMenu->setSaveStatus(true);
}
void AbstractTabDeckEditor::actEditDeckInClipboard()
{
editDeckInClipboard(true);
}
void AbstractTabDeckEditor::actEditDeckInClipboardRaw()
{
editDeckInClipboard(false);
}
void AbstractTabDeckEditor::actSaveDeckToClipboard()
{
getDeckList()->saveToClipboard(true, true);
}
void AbstractTabDeckEditor::actSaveDeckToClipboardNoSetInfo()
{
getDeckList()->saveToClipboard(true, false);
}
void AbstractTabDeckEditor::actSaveDeckToClipboardRaw()
{
getDeckList()->saveToClipboard(false, true);
}
void AbstractTabDeckEditor::actSaveDeckToClipboardRawNoSetInfo()
{
getDeckList()->saveToClipboard(false, false);
}
void AbstractTabDeckEditor::actPrintDeck()
{
auto *dlg = new QPrintPreviewDialog(this);
connect(dlg, SIGNAL(paintRequested(QPrinter *)), deckDockWidget->deckModel, SLOT(printDeckList(QPrinter *)));
dlg->exec();
}
void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website)
{
// check if deck is not null
if (DeckLoader *const deck = getDeckList()) {
// Get the decklist url string from the deck loader class.
QString decklistUrlString = deck->exportDeckToDecklist(website);
// Check to make sure the string isn't empty.
if (QString::compare(decklistUrlString, "", Qt::CaseInsensitive) == 0) {
// Show an error if the deck is empty, and return.
QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported"));
return;
}
// Encode the string recieved from the model to make sure all characters are encoded.
// first we put it into a qurl object
QUrl decklistUrl = QUrl(decklistUrlString);
// we get the correctly encoded url.
decklistUrlString = decklistUrl.toEncoded();
// We open the url in the user's default browser
QDesktopServices::openUrl(decklistUrlString);
} else {
// if there's no deck loader object, return an error
QMessageBox::critical(this, tr("Error"), tr("No deck was selected to be exported."));
}
}
/**
* Exports the deck to www.decklist.org (the old website)
*/
void AbstractTabDeckEditor::actExportDeckDecklist()
{
exportToDecklistWebsite(DeckLoader::DecklistOrg);
}
/**
* Exports the deck to www.decklist.xyz (the new website)
*/
void AbstractTabDeckEditor::actExportDeckDecklistXyz()
{
exportToDecklistWebsite(DeckLoader::DecklistXyz);
}
void AbstractTabDeckEditor::actAnalyzeDeckDeckstats()
{
auto *interface = new DeckStatsInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(),
this); // it deletes itself when done
interface->analyzeDeck(getDeckList());
}
void AbstractTabDeckEditor::actAnalyzeDeckTappedout()
{
auto *interface = new TappedOutInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(),
this); // it deletes itself when done
interface->analyzeDeck(getDeckList());
}
void AbstractTabDeckEditor::filterTreeChanged(FilterTree *filterTree)
{
databaseDisplayDockWidget->setFilterTree(filterTree);
}
// Method uses to sync docks state with menu items state
bool AbstractTabDeckEditor::eventFilter(QObject *o, QEvent *e)
{
if (e->type() == QEvent::Close) {
if (o == cardInfoDockWidget) {
aCardInfoDockVisible->setChecked(false);
aCardInfoDockFloating->setEnabled(false);
} else if (o == deckDockWidget) {
aDeckDockVisible->setChecked(false);
aDeckDockFloating->setEnabled(false);
} else if (o == filterDockWidget) {
aFilterDockVisible->setChecked(false);
aFilterDockFloating->setEnabled(false);
} else if (o == printingSelectorDockWidget) {
aPrintingSelectorDockVisible->setChecked(false);
aPrintingSelectorDockFloating->setEnabled(false);
}
}
if (o == this && e->type() == QEvent::Hide) {
LayoutsSettings &layouts = SettingsCache::instance().layouts();
layouts.setDeckEditorLayoutState(saveState());
layouts.setDeckEditorGeometry(saveGeometry());
layouts.setDeckEditorCardSize(cardInfoDockWidget->size());
layouts.setDeckEditorFilterSize(filterDockWidget->size());
layouts.setDeckEditorDeckSize(deckDockWidget->size());
layouts.setDeckEditorPrintingSelectorSize(printingSelectorDockWidget->size());
}
return false;
}
bool AbstractTabDeckEditor::confirmClose()
{
if (modified) {
tabSupervisor->setCurrentWidget(this);
int ret = createSaveConfirmationWindow()->exec();
if (ret == QMessageBox::Save)
return actSaveDeck();
else if (ret == QMessageBox::Cancel)
return false;
}
return true;
}
void AbstractTabDeckEditor::closeRequest(bool forced)
{
if (!forced && !confirmClose()) {
return;
}
emit deckEditorClosing(this);
close();
}

View File

@@ -0,0 +1,155 @@
#ifndef TAB_GENERIC_DECK_EDITOR_H
#define TAB_GENERIC_DECK_EDITOR_H
#include "../../game/cards/card_database.h"
#include "../menus/deck_editor/deck_editor_menu.h"
#include "../ui/widgets/deck_editor/deck_editor_card_info_dock_widget.h"
#include "../ui/widgets/deck_editor/deck_editor_database_display_widget.h"
#include "../ui/widgets/deck_editor/deck_editor_deck_dock_widget.h"
#include "../ui/widgets/deck_editor/deck_editor_filter_dock_widget.h"
#include "../ui/widgets/deck_editor/deck_editor_printing_selector_dock_widget.h"
#include "../ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include "tab.h"
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class CardInfoFrameWidget;
class DeckLoader;
class DeckEditorMenu;
class DeckEditorCardInfoDockWidget;
class DeckEditorDatabaseDisplayWidget;
class DeckEditorDeckDockWidget;
class DeckEditorFilterDockWidget;
class DeckEditorPrintingSelectorDockWidget;
class DeckPreviewDeckTagsDisplayWidget;
class Response;
class FilterTreeModel;
class FilterBuilder;
class QTreeView;
class QTextEdit;
class QLabel;
class QComboBox;
class QGroupBox;
class QMessageBox;
class QHBoxLayout;
class QVBoxLayout;
class QPushButton;
class QDockWidget;
class QMenu;
class QAction;
class AbstractTabDeckEditor : public Tab
{
Q_OBJECT
public:
explicit AbstractTabDeckEditor(TabSupervisor *_tabSupervisor);
// UI and Navigation
virtual void createMenus() = 0;
[[nodiscard]] virtual QString getTabText() const override = 0;
bool confirmClose();
virtual void retranslateUi() override = 0;
// Deck Management
void openDeck(DeckLoader *deck);
DeckLoader *getDeckList() const;
void setModified(bool _windowModified);
// UI Elements
DeckEditorMenu *deckMenu;
DeckEditorDatabaseDisplayWidget *databaseDisplayDockWidget;
DeckEditorCardInfoDockWidget *cardInfoDockWidget;
DeckEditorDeckDockWidget *deckDockWidget;
DeckEditorFilterDockWidget *filterDockWidget;
DeckEditorPrintingSelectorDockWidget *printingSelectorDockWidget;
public slots:
void onDeckChanged();
void updateCard(CardInfoPtr _card);
void actAddCard(CardInfoPtr info);
void actAddCardToSideboard(CardInfoPtr info);
void actDecrementCard(CardInfoPtr info);
void actDecrementCardFromSideboard(CardInfoPtr info);
void actOpenRecent(const QString &fileName);
void filterTreeChanged(FilterTree *filterTree);
void closeRequest(bool forced = false) override;
virtual void showPrintingSelector() = 0;
virtual void dockTopLevelChanged(bool topLevel) = 0;
signals:
void openDeckEditor(const DeckLoader *deckLoader);
void deckEditorClosing(AbstractTabDeckEditor *tab);
void decrementCard(CardInfoPtr card, QString zoneName);
protected slots:
// Deck Operations
virtual void actNewDeck();
void cleanDeckAndResetModified();
virtual void actLoadDeck();
bool actSaveDeck();
bool actSaveDeckAs();
virtual void actLoadDeckFromClipboard();
void actEditDeckInClipboard();
void actEditDeckInClipboardRaw();
void actSaveDeckToClipboard();
void actSaveDeckToClipboardNoSetInfo();
void actSaveDeckToClipboardRaw();
void actSaveDeckToClipboardRawNoSetInfo();
void actPrintDeck();
void actExportDeckDecklist();
void actExportDeckDecklistXyz();
void actAnalyzeDeckDeckstats();
void actAnalyzeDeckTappedout();
// Remote Save
void saveDeckRemoteFinished(const Response &r);
// UI Layout Management
virtual void loadLayout() = 0;
virtual void restartLayout() = 0;
virtual void freeDocksSize() = 0;
virtual void refreshShortcuts() = 0;
bool eventFilter(QObject *o, QEvent *e) override;
virtual void dockVisibleTriggered() = 0;
virtual void dockFloatingTriggered() = 0;
private:
virtual void setDeck(DeckLoader *_deck);
void editDeckInClipboard(bool annotated);
void exportToDecklistWebsite(DeckLoader::DecklistWebsite website);
protected:
/**
* @brief Enum for selecting deck open location
*/
enum DeckOpenLocation
{
CANCELLED,
SAME_TAB,
NEW_TAB
};
DeckOpenLocation confirmOpen(bool openInSameTabIfBlank = true);
QMessageBox *createSaveConfirmationWindow();
bool isBlankNewDeck() const;
// Helper functions for card actions
void addCardHelper(CardInfoPtr info, QString zoneName);
void actSwapCard(CardInfoPtr info, QString zoneName);
virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
// UI Menu Elements
QMenu *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu;
QAction *aResetLayout;
QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating;
QAction *aFilterDockVisible, *aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
bool modified = false;
};
#endif // TAB_GENERIC_DECK_EDITOR_H

File diff suppressed because it is too large Load Diff

View File

@@ -1,191 +1,41 @@
#ifndef WINDOW_DECKEDITOR_H
#define WINDOW_DECKEDITOR_H
#include "../../deck/custom_line_edit.h"
#include "../../game/cards/card_database.h"
#include "../game_logic/key_signals.h"
#include "../ui/widgets/printing_selector/printing_selector.h"
#include "tab.h"
#include <QAbstractItemModel>
#include <QDir>
#include "../ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include "abstract_tab_deck_editor.h"
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class DeckListModel;
class QTreeView;
class CardInfoFrameWidget;
class QTextEdit;
class QLabel;
class DeckLoader;
class Response;
class FilterTreeModel;
class FilterBuilder;
class QComboBox;
class QGroupBox;
class QMessageBox;
class QHBoxLayout;
class QVBoxLayout;
class QPushButton;
class QDockWidget;
class TabDeckEditor : public Tab
class TabDeckEditor : public AbstractTabDeckEditor
{
Q_OBJECT
private slots:
void updateName(const QString &name);
void updateComments();
void updateBannerCardComboBox();
void setBannerCard(int);
void updateHash();
void updateCardInfoLeft(const QModelIndex &current, const QModelIndex &previous);
void updateCardInfoRight(const QModelIndex &current, const QModelIndex &previous);
void updatePrintingSelectorDatabase(const QModelIndex &current, const QModelIndex &previous);
void updatePrintingSelectorDeckView(const QModelIndex &current, const QModelIndex &previous);
void updateSearch(const QString &search);
void databaseCustomMenu(QPoint point);
void decklistCustomMenu(QPoint point);
void updateRecentlyOpened();
void actNewDeck();
void actLoadDeck();
void actOpenRecent(const QString &fileName);
void actClearRecents();
bool actSaveDeck();
bool actSaveDeckAs();
void actLoadDeckFromClipboard();
void actSaveDeckToClipboard();
void actSaveDeckToClipboardNoSetNameAndNumber();
void actSaveDeckToClipboardRaw();
void actSaveDeckToClipboardRawNoSetNameAndNumber();
void actPrintDeck();
void actExportDeckDecklist();
void actAnalyzeDeckDeckstats();
void actAnalyzeDeckTappedout();
void actClearFilterAll();
void actClearFilterOne();
void actSwapCard();
void actAddCard();
void actAddCardToSideboard();
void actRemoveCard();
void actIncrement();
void actDecrement();
void actDecrementCard();
void actDecrementCardFromSideboard();
void copyDatabaseCellContents();
void saveDeckRemoteFinished(const Response &r);
void filterViewCustomContextMenu(const QPoint &point);
void filterRemove(QAction *action);
void loadLayout();
void restartLayout();
void freeDocksSize();
void refreshShortcuts();
protected slots:
void loadLayout() override;
void restartLayout() override;
void freeDocksSize() override;
void refreshShortcuts() override;
bool eventFilter(QObject *o, QEvent *e) override;
void dockVisibleTriggered();
void dockFloatingTriggered();
void dockTopLevelChanged(bool topLevel);
void saveDbHeaderState();
void setSaveStatus(bool newStatus);
void showSearchSyntaxHelp();
private:
/**
* @brief Which tab to open the new deck in
*/
enum DeckOpenLocation
{
CANCELLED,
SAME_TAB,
NEW_TAB
};
DeckOpenLocation confirmOpen(const bool openInSameTabIfBlank = true);
QMessageBox *createSaveConfirmationWindow();
bool isBlankNewDeck() const;
CardInfoPtr currentCardInfo() const;
void offsetCountAtIndex(const QModelIndex &idx, int offset);
void decrementCardHelper(QString zoneName);
bool swapCard(const QModelIndex &idx);
void recursiveExpand(const QModelIndex &index);
void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
QModelIndexList getSelectedCardNodes() const;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
DeckListModel *deckModel;
QTreeView *databaseView;
QTreeView *deckView;
KeySignals deckViewKeySignals;
CardInfoFrameWidget *cardInfo;
PrintingSelector *printingSelector;
SearchLineEdit *searchEdit;
KeySignals searchKeySignals;
QLabel *nameLabel;
LineEditUnfocusable *nameEdit;
QLabel *commentsLabel;
QTextEdit *commentsEdit;
QLabel *bannerCardLabel;
QComboBox *bannerCardComboBox;
QLabel *hashLabel1;
LineEditUnfocusable *hashLabel;
FilterTreeModel *filterModel;
QTreeView *filterView;
KeySignals filterViewKeySignals;
QWidget *filterBox;
QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu,
*analyzeDeckMenu, *saveDeckToClipboardMenu, *loadRecentDeckMenu;
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetNameAndNumber, *aSaveDeckToClipboardRaw,
*aSaveDeckToClipboardRawNoSetNameAndNumber, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats,
*aAnalyzeDeckTappedout, *aClose;
QAction *aClearFilterAll, *aClearFilterOne;
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
QAction *aResetLayout;
QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, *aFilterDockVisible,
*aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
bool modified;
QVBoxLayout *centralFrame;
QHBoxLayout *searchLayout;
QDockWidget *cardInfoDock;
QDockWidget *deckDock;
QDockWidget *filterDock;
QDockWidget *printingSelectorDock;
QWidget *centralWidget;
void dockVisibleTriggered() override;
void dockFloatingTriggered() override;
void dockTopLevelChanged(bool topLevel) override;
public:
explicit TabDeckEditor(TabSupervisor *_tabSupervisor);
void retranslateUi() override;
QString getTabText() const override;
void setDeck(DeckLoader *_deckLoader);
void setModified(bool _windowModified);
bool confirmClose();
void createDeckDock();
void createCardInfoDock();
void createFiltersDock();
void createPrintingSelectorDock();
void createMenus();
void createCentralFrame();
void updateCardInfo(CardInfoPtr _card);
void addCardHelper(CardInfoPtr info, QString zoneName);
void createMenus() override;
public slots:
void closeRequest(bool forced = false) override;
void showPrintingSelector();
signals:
void openDeckEditor(const DeckLoader *deckLoader);
void deckEditorClosing(TabDeckEditor *tab);
void showPrintingSelector() override;
};
#endif

View File

@@ -106,6 +106,9 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
aOpenLocalDeck = new QAction(this);
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/rename"));
connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this);
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
connect(aUpload, SIGNAL(triggered()), this, SLOT(actUpload()));
@@ -136,6 +139,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Add actions to toolbars
leftToolBar->addAction(aOpenLocalDeck);
leftToolBar->addAction(aRenameLocal);
leftToolBar->addAction(aUpload);
leftToolBar->addAction(aNewLocalFolder);
leftToolBar->addAction(aDeleteLocalDeck);
@@ -164,6 +168,7 @@ void TabDeckStorage::retranslateUi()
rightGroupBox->setTitle(tr("Server deck storage"));
aOpenLocalDeck->setText(tr("Open in deck editor"));
aRenameLocal->setText(tr("Rename deck or folder"));
aUpload->setText(tr("Upload deck"));
aOpenRemoteDeck->setText(tr("Open in deck editor"));
aDownload->setText(tr("Download deck"));
@@ -241,12 +246,40 @@ void TabDeckStorage::actOpenLocalDeck()
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
continue;
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(filePath);
emit openDeckEditor(&deckLoader);
}
}
void TabDeckStorage::actRenameLocal()
{
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
for (const auto &curLeft : curLefts) {
const QFileInfo info = localDirModel->fileInfo(curLeft);
const QString oldName = info.baseName();
const QString title = info.isDir() ? tr("Rename local folder") : tr("Rename local file");
bool ok;
QString newName = QInputDialog::getText(this, title, tr("New name:"), QLineEdit::Normal, oldName, &ok);
if (!ok) { // terminate all remaining selections if user cancels
return;
}
if (newName.isEmpty() || oldName == newName) {
continue;
}
QString newFileName = newName;
if (!info.suffix().isEmpty()) {
newFileName += "." + info.suffix();
}
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
if (!QFile::rename(info.filePath(), newFilePath)) {
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
}
}
}
void TabDeckStorage::actUpload()
{
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();

View File

@@ -28,7 +28,7 @@ private:
RemoteDeckList_TreeWidget *serverDirView;
QGroupBox *leftGroupBox, *rightGroupBox;
QAction *aOpenLocalDeck, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
QAction *aOpenDecksFolder;
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
QString getTargetPath() const;
@@ -47,6 +47,8 @@ private slots:
void actLocalDoubleClick(const QModelIndex &curLeft);
void actOpenLocalDeck();
void actRenameLocal();
void actUpload();
void uploadFinished(const Response &r, const CommandContainer &commandContainer);

View File

@@ -119,7 +119,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
refreshShortcuts();
messageLog->logReplayStarted(gameInfo.game_id());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
@@ -164,7 +163,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
@@ -506,10 +504,12 @@ void TabGame::actConcede()
bool TabGame::leaveGame()
{
if (!gameClosed) {
if (!spectator)
if (!spectator) {
tabSupervisor->setCurrentWidget(this);
if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
return false;
}
if (!replay)
sendGameCommand(Command_LeaveGame());
@@ -753,7 +753,7 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont,
default: {
Player *player = players.value(playerId, 0);
if (!player) {
qCDebug(TabGameLog) << "unhandled game event: invalid player id";
qCWarning(TabGameLog) << "unhandled game event: invalid player id";
break;
}
player->processGameEvent(eventType, event, context, options);
@@ -836,6 +836,7 @@ void TabGame::startGame(bool _resuming)
while (i.hasNext()) {
i.next();
i.value()->setReadyStart(false);
i.value()->setVisualDeckStorageExists(false);
i.value()->hide();
}
@@ -928,8 +929,8 @@ void TabGame::eventGameStateChanged(const Event_GameStateChanged &event,
DeckViewContainer *deckViewContainer = deckViewContainers.value(playerId);
if (playerInfo.has_deck_list()) {
DeckLoader newDeck(QString::fromStdString(playerInfo.deck_list()));
PictureLoader::cacheCardPixmaps(
CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCardsByNameAndProviderId(
newDeck.getCardListWithProviderId()));
deckViewContainer->setDeck(newDeck);
player->setDeck(newDeck);
}
@@ -1750,6 +1751,27 @@ void TabGame::createMessageDock(bool bReplay)
connect(messageLayoutDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool)));
}
void TabGame::hideEvent(QHideEvent *event)
{
LayoutsSettings &layouts = SettingsCache::instance().layouts();
if (replay) {
layouts.setReplayPlayAreaState(saveState());
layouts.setReplayPlayAreaGeometry(saveGeometry());
layouts.setReplayCardInfoSize(cardInfoDock->size());
layouts.setReplayMessageLayoutSize(messageLayoutDock->size());
layouts.setReplayPlayerListSize(playerListDock->size());
layouts.setReplayReplaySize(replayDock->size());
} else {
layouts.setGamePlayAreaState(saveState());
layouts.setGamePlayAreaGeometry(saveGeometry());
layouts.setGameCardInfoSize(cardInfoDock->size());
layouts.setGameMessageLayoutSize(messageLayoutDock->size());
layouts.setGamePlayerListSize(playerListDock->size());
}
Tab::hideEvent(event);
}
// Method uses to sync docks state with menu items state
bool TabGame::eventFilter(QObject *o, QEvent *e)
{
@@ -1769,23 +1791,6 @@ bool TabGame::eventFilter(QObject *o, QEvent *e)
}
}
if (o == this && e->type() == QEvent::Hide) {
LayoutsSettings &layouts = SettingsCache::instance().layouts();
if (replay) {
layouts.setReplayPlayAreaState(saveState());
layouts.setReplayPlayAreaGeometry(saveGeometry());
layouts.setReplayCardInfoSize(cardInfoDock->size());
layouts.setReplayMessageLayoutSize(messageLayoutDock->size());
layouts.setReplayPlayerListSize(playerListDock->size());
layouts.setReplayReplaySize(replayDock->size());
} else {
layouts.setGamePlayAreaState(saveState());
layouts.setGamePlayAreaGeometry(saveGeometry());
layouts.setGameCardInfoSize(cardInfoDock->size());
layouts.setGameMessageLayoutSize(messageLayoutDock->size());
layouts.setGamePlayerListSize(playerListDock->size());
}
}
return false;
}

View File

@@ -208,6 +208,7 @@ private slots:
void actResetLayout();
void freeDocksSize();
void hideEvent(QHideEvent *event) override;
bool eventFilter(QObject *o, QEvent *e) override;
void dockVisibleTriggered();
void dockFloatingTriggered();

View File

@@ -145,7 +145,7 @@ void TabMessage::showSystemPopup(const Event_UserMessage &event)
event.message().c_str());
connect(trayIcon, SIGNAL(messageClicked()), this, SLOT(messageClicked()));
} else {
qCDebug(TabMessageLog) << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
qCWarning(TabMessageLog) << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
}
}

View File

@@ -99,7 +99,7 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, c
connect(aOpenLocalReplay, SIGNAL(triggered()), this, SLOT(actOpenLocalReplay()));
connect(localDirView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actOpenLocalReplay()));
aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/pencil"));
aRenameLocal->setIcon(QPixmap("theme:icons/rename"));
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));

View File

@@ -201,7 +201,7 @@ void TabSupervisor::retranslateUi()
QListIterator<TabGame *> replayIterator(replayTabs);
while (replayIterator.hasNext())
tabs.append(replayIterator.next());
QListIterator<TabDeckEditor *> deckEditorIterator(deckEditorTabs);
QListIterator<AbstractTabDeckEditor *> deckEditorIterator(deckEditorTabs);
while (deckEditorIterator.hasNext())
tabs.append(deckEditorIterator.next());
QMapIterator<QString, TabMessage *> messageIterator(messageTabs);
@@ -232,22 +232,45 @@ void TabSupervisor::refreshShortcuts()
aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog"));
}
bool TabSupervisor::closeRequest()
void TabSupervisor::closeEvent(QCloseEvent *event)
{
// This will accept the event, which we may then override.
QTabWidget::closeEvent(event);
if (getGameCount()) {
if (QMessageBox::question(this, tr("Are you sure?"),
tr("There are still open games. Are you sure you want to quit?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
return false;
event->ignore();
return;
}
}
for (TabDeckEditor *tab : deckEditorTabs) {
if (!tab->confirmClose())
return false;
for (AbstractTabDeckEditor *tab : deckEditorTabs) {
if (!tab->confirmClose()) {
event->ignore();
}
}
return true;
// Close the game tabs in order to make sure they store their layout.
QSet<int> gameTabsToRemove;
for (auto it = gameTabs.begin(), end = gameTabs.end(); it != end; ++it) {
if (it.value()->close()) {
// Hotfix: the tab owns the `QMenu`s so they need to be cleared,
// otherwise we end up with use-after-free bugs.
if (it.value() == currentWidget()) {
emit setMenu();
}
gameTabsToRemove.insert(it.key());
} else {
event->ignore();
}
}
for (auto tabId : gameTabsToRemove) {
gameTabs.remove(tabId);
}
}
AbstractClient *TabSupervisor::getClient() const
@@ -285,9 +308,15 @@ void TabSupervisor::initStartupTabs()
{
addDeckEditorTab(nullptr);
checkAndTrigger(aTabVisualDeckStorage, SettingsCache::instance().getTabVisualDeckStorageOpen());
checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen());
checkAndTrigger(aTabReplays, SettingsCache::instance().getTabReplaysOpen());
if (SettingsCache::instance().getTabVisualDeckStorageOpen()) {
openTabVisualDeckStorage();
}
if (SettingsCache::instance().getTabDeckStorageOpen()) {
openTabDeckStorage();
}
if (SettingsCache::instance().getTabReplaysOpen()) {
openTabReplays();
}
}
/**
@@ -360,8 +389,12 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
tabsMenu->addAction(aTabServer);
tabsMenu->addAction(aTabAccount);
checkAndTrigger(aTabServer, SettingsCache::instance().getTabServerOpen());
checkAndTrigger(aTabAccount, SettingsCache::instance().getTabAccountOpen());
if (SettingsCache::instance().getTabServerOpen()) {
openTabServer();
}
if (SettingsCache::instance().getTabAccountOpen()) {
openTabAccount();
}
updatePingTime(0, -1);
@@ -370,8 +403,12 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
tabsMenu->addAction(aTabAdmin);
tabsMenu->addAction(aTabLog);
checkAndTrigger(aTabAdmin, SettingsCache::instance().getTabAdminOpen());
checkAndTrigger(aTabLog, SettingsCache::instance().getTabLogOpen());
if (SettingsCache::instance().getTabAdminOpen()) {
openTabAdmin();
}
if (SettingsCache::instance().getTabLogOpen()) {
openTabLog();
}
}
retranslateUi();
@@ -453,114 +490,163 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked)
{
SettingsCache::instance().setTabVisualDeckStorageOpen(checked);
if (checked && !tabVisualDeckStorage) {
tabVisualDeckStorage = new TabDeckStorageVisual(this);
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
connect(tabVisualDeckStorage, &Tab::closed, this, [this] {
tabVisualDeckStorage = nullptr;
aTabVisualDeckStorage->setChecked(false);
});
openTabVisualDeckStorage();
setCurrentWidget(tabVisualDeckStorage);
} else if (!checked && tabVisualDeckStorage) {
tabVisualDeckStorage->closeRequest();
}
}
void TabSupervisor::openTabVisualDeckStorage()
{
tabVisualDeckStorage = new TabDeckStorageVisual(this);
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
connect(tabVisualDeckStorage, &Tab::closed, this, [this] {
tabVisualDeckStorage = nullptr;
aTabVisualDeckStorage->setChecked(false);
});
aTabVisualDeckStorage->setChecked(true);
}
void TabSupervisor::actTabServer(bool checked)
{
SettingsCache::instance().setTabServerOpen(checked);
if (checked && !tabServer) {
tabServer = new TabServer(this, client);
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
myAddTab(tabServer, aTabServer);
connect(tabServer, &Tab::closed, this, [this] {
tabServer = nullptr;
aTabServer->setChecked(false);
});
openTabServer();
setCurrentWidget(tabServer);
} else if (!checked && tabServer) {
tabServer->closeRequest();
}
}
void TabSupervisor::openTabServer()
{
tabServer = new TabServer(this, client);
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
myAddTab(tabServer, aTabServer);
connect(tabServer, &Tab::closed, this, [this] {
tabServer = nullptr;
aTabServer->setChecked(false);
});
aTabServer->setChecked(true);
}
void TabSupervisor::actTabAccount(bool checked)
{
SettingsCache::instance().setTabAccountOpen(checked);
if (checked && !tabAccount) {
tabAccount = new TabAccount(this, client, *userInfo);
connect(tabAccount, &TabAccount::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tabAccount, &TabAccount::userJoined, this, &TabSupervisor::processUserJoined);
connect(tabAccount, &TabAccount::userLeft, this, &TabSupervisor::processUserLeft);
myAddTab(tabAccount, aTabAccount);
connect(tabAccount, &Tab::closed, this, [this] {
tabAccount = nullptr;
aTabAccount->setChecked(false);
});
openTabAccount();
setCurrentWidget(tabAccount);
} else if (!checked && tabAccount) {
tabAccount->closeRequest();
}
}
void TabSupervisor::openTabAccount()
{
tabAccount = new TabAccount(this, client, *userInfo);
connect(tabAccount, &TabAccount::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tabAccount, &TabAccount::userJoined, this, &TabSupervisor::processUserJoined);
connect(tabAccount, &TabAccount::userLeft, this, &TabSupervisor::processUserLeft);
myAddTab(tabAccount, aTabAccount);
connect(tabAccount, &Tab::closed, this, [this] {
tabAccount = nullptr;
aTabAccount->setChecked(false);
});
aTabAccount->setChecked(true);
}
void TabSupervisor::actTabDeckStorage(bool checked)
{
SettingsCache::instance().setTabDeckStorageOpen(checked);
if (checked && !tabDeckStorage) {
tabDeckStorage = new TabDeckStorage(this, client, userInfo);
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tabDeckStorage, aTabDeckStorage);
connect(tabDeckStorage, &Tab::closed, this, [this] {
tabDeckStorage = nullptr;
aTabDeckStorage->setChecked(false);
});
openTabDeckStorage();
setCurrentWidget(tabDeckStorage);
} else if (!checked && tabDeckStorage) {
tabDeckStorage->closeRequest();
}
}
void TabSupervisor::openTabDeckStorage()
{
tabDeckStorage = new TabDeckStorage(this, client, userInfo);
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tabDeckStorage, aTabDeckStorage);
connect(tabDeckStorage, &Tab::closed, this, [this] {
tabDeckStorage = nullptr;
aTabDeckStorage->setChecked(false);
});
aTabDeckStorage->setChecked(true);
}
void TabSupervisor::actTabReplays(bool checked)
{
SettingsCache::instance().setTabReplaysOpen(checked);
if (checked && !tabReplays) {
tabReplays = new TabReplays(this, client, userInfo);
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
myAddTab(tabReplays, aTabReplays);
connect(tabReplays, &Tab::closed, this, [this] {
tabReplays = nullptr;
aTabReplays->setChecked(false);
});
openTabReplays();
setCurrentWidget(tabReplays);
} else if (!checked && tabReplays) {
tabReplays->closeRequest();
}
}
void TabSupervisor::openTabReplays()
{
tabReplays = new TabReplays(this, client, userInfo);
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
myAddTab(tabReplays, aTabReplays);
connect(tabReplays, &Tab::closed, this, [this] {
tabReplays = nullptr;
aTabReplays->setChecked(false);
});
aTabReplays->setChecked(true);
}
void TabSupervisor::actTabAdmin(bool checked)
{
SettingsCache::instance().setTabAdminOpen(checked);
if (checked && !tabAdmin) {
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
myAddTab(tabAdmin, aTabAdmin);
connect(tabAdmin, &Tab::closed, this, [this] {
tabAdmin = nullptr;
aTabAdmin->setChecked(false);
});
openTabAdmin();
setCurrentWidget(tabAdmin);
} else if (!checked && tabAdmin) {
tabAdmin->closeRequest();
}
}
void TabSupervisor::openTabAdmin()
{
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
myAddTab(tabAdmin, aTabAdmin);
connect(tabAdmin, &Tab::closed, this, [this] {
tabAdmin = nullptr;
aTabAdmin->setChecked(false);
});
aTabAdmin->setChecked(true);
}
void TabSupervisor::actTabLog(bool checked)
{
SettingsCache::instance().setTabLogOpen(checked);
if (checked && !tabLog) {
tabLog = new TabLog(this, client);
myAddTab(tabLog, aTabLog);
connect(tabLog, &Tab::closed, this, [this] {
tabLog = nullptr;
aTabAdmin->setChecked(false);
});
openTabLog();
setCurrentWidget(tabLog);
} else if (!checked && tabLog) {
tabLog->closeRequest();
}
}
void TabSupervisor::openTabLog()
{
tabLog = new TabLog(this, client);
myAddTab(tabLog, aTabLog);
connect(tabLog, &Tab::closed, this, [this] {
tabLog = nullptr;
aTabAdmin->setChecked(false);
});
aTabAdmin->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max)
{
if (!tabServer)
@@ -701,13 +787,17 @@ void TabSupervisor::talkLeft(TabMessage *tab)
removeTab(indexOf(tab));
}
/**
* Creates a new deck editor tab
* @param deckToOpen The deck to open in the tab. Creates a copy of the DeckLoader instance.
*/
TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
{
auto *tab = new TabDeckEditor(this);
if (deckToOpen)
tab->setDeck(new DeckLoader(*deckToOpen));
connect(tab, &TabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
connect(tab, &TabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
tab->openDeck(new DeckLoader(*deckToOpen));
connect(tab, &AbstractTabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
connect(tab, &AbstractTabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tab);
deckEditorTabs.append(tab);
setCurrentWidget(tab);
@@ -726,7 +816,7 @@ TabEdhRec *TabSupervisor::addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCo
return tab;
}
void TabSupervisor::deckEditorClosed(TabDeckEditor *tab)
void TabSupervisor::deckEditorClosed(AbstractTabDeckEditor *tab)
{
if (tab == currentWidget())
emit setMenu();
@@ -766,7 +856,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
if (tab)
tab->processGameEventContainer(cont, qobject_cast<AbstractClient *>(sender()), {});
else
qCDebug(TabSupervisorLog) << "gameEvent: invalid gameId";
qCInfo(TabSupervisorLog) << "gameEvent: invalid gameId" << cont.game_id();
}
void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)

View File

@@ -3,6 +3,7 @@
#include "../../deck/deck_loader.h"
#include "../../server/user/user_list_proxy.h"
#include "abstract_tab_deck_editor.h"
#include "api/edhrec/tab_edhrec.h"
#include "visual_deck_storage/tab_deck_storage_visual.h"
@@ -87,7 +88,7 @@ private:
QMap<int, TabGame *> gameTabs;
QList<TabGame *> replayTabs;
QMap<QString, TabMessage *> messageTabs;
QList<TabDeckEditor *> deckEditorTabs;
QList<AbstractTabDeckEditor *> deckEditorTabs;
bool isLocalGame;
QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays,
@@ -132,12 +133,12 @@ public:
{
return roomTabs;
}
QList<TabDeckEditor *> getDeckEditorTabs() const
QList<AbstractTabDeckEditor *> getDeckEditorTabs() const
{
return deckEditorTabs;
}
bool getAdminLocked() const;
bool closeRequest();
void closeEvent(QCloseEvent *event) override;
bool switchToGameTabIfAlreadyExists(const int gameId);
static void actShowPopup(const QString &message);
signals:
@@ -162,6 +163,14 @@ private slots:
void actTabAdmin(bool checked);
void actTabLog(bool checked);
void openTabVisualDeckStorage();
void openTabServer();
void openTabAccount();
void openTabDeckStorage();
void openTabReplays();
void openTabAdmin();
void openTabLog();
void updateCurrent(int index);
void updatePingTime(int value, int max);
void gameJoined(const Event_GameJoined &event);
@@ -174,7 +183,7 @@ private slots:
void processUserLeft(const QString &userName);
void processUserJoined(const ServerInfo_User &userInfo);
void talkLeft(TabMessage *tab);
void deckEditorClosed(TabDeckEditor *tab);
void deckEditorClosed(AbstractTabDeckEditor *tab);
void tabUserEvent(bool globalEvent);
void updateTabText(Tab *tab, const QString &newTabText);
void processRoomEvent(const RoomEvent &event);
@@ -183,4 +192,4 @@ private slots:
void processNotifyUserEvent(const Event_NotifyUser &event);
};
#endif
#endif

View File

@@ -6,14 +6,17 @@
#include "../tab_supervisor.h"
#include "pb/command_deck_del.pb.h"
#include <QMessageBox>
#include <QMouseEvent>
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
{
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this,
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this,
&TabDeckStorageVisual::actOpenLocalDeck);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
&TabDeckStorageVisual::openDeckEditor);
auto *widget = new QWidget(this);
auto *layout = new QVBoxLayout(widget);
@@ -22,10 +25,11 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
layout->addWidget(visualDeckStorageWidget);
}
void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance)
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
{
DeckLoader deckLoader;
if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::getFormatFromName(instance->filePath), true)) {
if (!deckLoader.loadFromFile(filePath, DeckLoader::getFormatFromName(filePath), true)) {
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(filePath));
return;
}

View File

@@ -28,7 +28,7 @@ public:
}
public slots:
void actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance);
void actOpenLocalDeck(const QString &filePath);
signals:
void openDeckEditor(const DeckLoader *deckLoader);

View File

@@ -33,7 +33,7 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
* can be extracted from the header. The http status is a 302 "redirect".
*/
QString deckUrl = reply->rawHeader("Location");
qCDebug(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
qCInfo(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
QDesktopServices::openUrl("https://tappedout.net" + deckUrl);
} else {
/*
@@ -57,8 +57,8 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
}
QString errorMessage = errorMessageList.join("\n");
qCDebug(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
<< "message" << errorMessage;
qCWarning(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
<< "message" << errorMessage;
QMessageBox::critical(nullptr, tr("Error"), errorMessage);
}

View File

@@ -7,7 +7,7 @@
#include <QWidget>
#include <qstyle.h>
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout");
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg);
class FlowLayout : public QLayout
{

View File

@@ -37,51 +37,92 @@ PictureLoader::~PictureLoader()
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
qCDebug(PictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(PictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::gray); // Fallback to a gray pixmap
} else {
qCDebug(PictureLoaderLog) << "Successfully loaded 'theme:cardback'.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
QString backCacheKey =
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(PictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::blue); // Fallback with blue color
} else {
qCDebug(PictureLoaderCardBackCacheFailLog) << "Successfully loaded 'theme:cardback' for in-progress state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
QString backCacheKey =
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback");
if (tmpPixmap.isNull()) {
qCWarning(PictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::red); // Fallback with red color
} else {
qCDebug(PictureLoaderCardBackCacheFailLog) << "Successfully loaded 'theme:cardback' for failed state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
{
if (card == nullptr) {
if (!card) {
qCWarning(PictureLoaderLog) << "getPixmap called with null card!";
return;
}
// search for an exact size copy of the picture in cache
QString key = card->getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap))
return;
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + "x" + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap)) {
return; // Use cached version
}
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) {
qCDebug(PictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!";
return;
}
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen->devicePixelRatio();
qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
qCDebug(PictureLoaderLog) << "Scaling cached image for" << card->getName();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
@@ -154,3 +195,31 @@ void PictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}
bool PictureLoader::hasCustomArt()
{
auto picsPath = SettingsCache::instance().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other
// than in the "downloadedPics" subdirectory.
while (it.hasNext()) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
QFileInfo dir(it.nextFileInfo());
#else
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
// the QFileInfo from a QString which requires more system calls.
QFileInfo dir(it.next());
#endif
if (it.fileName() == "downloadedPics")
continue;
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (subIt.hasNext()) {
return true;
}
}
return false;
}

View File

@@ -36,6 +36,7 @@ public:
static void clearPixmapCache(CardInfoPtr card);
static void clearPixmapCache();
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
static bool hasCustomArt();
public slots:
static void clearNetworkCache();

View File

@@ -16,11 +16,14 @@ QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a90
PictureLoaderWorker::PictureLoaderWorker()
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
{
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
@@ -83,11 +86,24 @@ void PictureLoaderWorker::processLoadQueue()
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
if (cardImageExistsOnDisk(setName, correctedCardName)) {
continue;
}
// FIXME: This is a hack so that to keep old Cockatrice behavior
// (ignoring provider ID) when the "override all card art with personal
// preference" is set.
//
// Figure out a proper way to integrate the two systems at some point.
//
// Note: need to go through a member for
// overrideAllCardArtWithPersonalPreference as reading from the
// SettingsCache instance from the PictureLoaderWorker thread could
// cause race conditions.
//
// XXX: Reading from the CardDatabaseManager instance from the
// PictureLoaderWorker thread might not be safe either
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
continue;
}
qCDebug(PictureLoaderWorkerLog).nospace()
@@ -100,23 +116,26 @@ void PictureLoaderWorker::processLoadQueue()
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname, bool searchCustomPics)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (searchCustomPics) {
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
}
@@ -194,7 +213,7 @@ void PictureLoaderWorker::picDownloadFailed()
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
qCWarning(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
@@ -405,7 +424,7 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
}
if (logSuccessMessage) {
qCDebug(PictureLoaderWorkerLog).nospace()
qCInfo(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
@@ -451,7 +470,12 @@ void PictureLoaderWorker::picsPathChanged()
customPicsPath = SettingsCache::instance().getCustomPicsPath();
}
void PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference)
{
overrideAllCardArtWithPersonalPreference = _overrideAllCardArtWithPersonalPreference;
}
void PictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
}
}

View File

@@ -42,8 +42,16 @@ private:
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning;
bool overrideAllCardArtWithPersonalPreference;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
disk, return `false` otherwise.
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
matching image first; otherwise, only the set-based folders are used. */
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
@@ -58,6 +66,7 @@ private slots:
void picDownloadChanged();
void picsPathChanged();
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
public slots:
void processLoadQueue();

View File

@@ -180,6 +180,12 @@ QMap<QString, QPixmap> CountryPixmapGenerator::pmCache;
* @param idName id that the tag has to match
* @param attrValue the value to update the attribute to
*/
static void setAttrRecur(QDomElement &elem,
const QString &tagName,
const QString &attrName,
const QString &idName,
const QString &attrValue);
void setAttrRecur(QDomElement &elem,
const QString &tagName,
const QString &attrName,
@@ -357,6 +363,21 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
QMap<int, QPixmap> LockPixmapGenerator::pmCache;
QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
{
QString key = QString::number(expanded) + ":" + QString::number(height);
if (pmCache.contains(key))
return pmCache.value(key);
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
pmCache.insert(key, pixmap);
return pixmap;
}
QMap<QString, QPixmap> DropdownIconPixmapGenerator::pmCache;
QPixmap loadColorAdjustedPixmap(const QString &name)
{
if (qApp->palette().windowText().color().lightness() > 200) {

View File

@@ -106,6 +106,19 @@ public:
}
};
class DropdownIconPixmapGenerator
{
private:
static QMap<QString, QPixmap> pmCache;
public:
static QPixmap generatePixmap(int height, bool expanded);
static void clear()
{
pmCache.clear();
}
};
QPixmap loadColorAdjustedPixmap(const QString &name);
#endif

View File

@@ -32,7 +32,7 @@ void ThemeManager::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
qCDebug(ThemeManagerLog) << "Theme name not set, setting default value";
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
}
}
@@ -105,7 +105,7 @@ QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
void ThemeManager::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getThemeName();
qCDebug(ThemeManagerLog) << "Theme changed:" << themeName;
qCInfo(ThemeManagerLog) << "Theme changed:" << themeName;
QString dirPath = getAvailableThemes().value(themeName);
QDir dir = dirPath;
@@ -120,10 +120,10 @@ void ThemeManager::themeChangedSlot()
if (dirPath.isEmpty()) {
// set default values
QDir::setSearchPaths("theme", DEFAULT_RESOURCE_PATHS);
handBgBrush = HANDZONE_BG_DEFAULT;
tableBgBrush = TABLEZONE_BG_DEFAULT;
playerBgBrush = PLAYERZONE_BG_DEFAULT;
stackBgBrush = STACKZONE_BG_DEFAULT;
brushes[Role::Hand] = HANDZONE_BG_DEFAULT;
brushes[Role::Table] = TABLEZONE_BG_DEFAULT;
brushes[Role::Player] = PLAYERZONE_BG_DEFAULT;
brushes[Role::Stack] = STACKZONE_BG_DEFAULT;
} else {
// resources
QStringList resources;
@@ -132,73 +132,58 @@ void ThemeManager::themeChangedSlot()
// zones bg
dir.cd("zones");
handBgBrush = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
tableBgBrush = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
playerBgBrush = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
stackBgBrush = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
}
for (auto &brushCache : brushesCache) {
brushCache.clear();
}
tableBgBrushesCache.clear();
stackBgBrushesCache.clear();
playerBgBrushesCache.clear();
handBgBrushesCache.clear();
QPixmapCache::clear();
emit themeChanged();
}
QBrush ThemeManager::getExtraTableBgBrush(QString extraNumber, QBrush &fallbackBrush)
static QString roleBgName(ThemeManager::Role role)
{
QBrush returnBrush;
switch (role) {
case ThemeManager::Hand:
return HANDZONE_BG_NAME;
if (!tableBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(TABLEZONE_BG_NAME + extraNumber, fallbackBrush);
tableBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = tableBgBrushesCache.value(extraNumber.toInt());
case ThemeManager::Player:
return PLAYERZONE_BG_NAME;
case ThemeManager::Stack:
return STACKZONE_BG_NAME;
case ThemeManager::Table:
return TABLEZONE_BG_NAME;
default:
Q_ASSERT(false);
}
return returnBrush;
}
QBrush ThemeManager::getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush)
QBrush &ThemeManager::getBgBrush(Role role)
{
QBrush returnBrush;
if (!stackBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(STACKZONE_BG_NAME + extraNumber, fallbackBrush);
stackBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = stackBgBrushesCache.value(extraNumber.toInt());
}
return returnBrush;
return brushes[role];
}
QBrush ThemeManager::getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush)
QBrush ThemeManager::getExtraBgBrush(Role role, int zoneId)
{
QBrush returnBrush;
if (!playerBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(PLAYERZONE_BG_NAME + extraNumber, fallbackBrush);
playerBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = playerBgBrushesCache.value(extraNumber.toInt());
if (zoneId <= 0) {
return getBgBrush(role);
}
return returnBrush;
}
QBrushMap &brushCache = brushesCache[role];
QBrush ThemeManager::getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush)
{
QBrush returnBrush;
if (!handBgBrushesCache.contains(extraNumber.toInt())) {
returnBrush = loadExtraBrush(HANDZONE_BG_NAME + extraNumber, fallbackBrush);
handBgBrushesCache.insert(extraNumber.toInt(), returnBrush);
} else {
returnBrush = handBgBrushesCache.value(extraNumber.toInt());
if (!brushCache.contains(zoneId)) {
QBrush brush = loadExtraBrush(roleBgName(role) + QString::number(zoneId), getBgBrush(role));
brushCache.insert(zoneId, brush);
return brush;
}
return returnBrush;
return brushCache.value(zoneId);
}

View File

@@ -8,6 +8,7 @@
#include <QObject>
#include <QPixmap>
#include <QString>
#include <array>
inline Q_LOGGING_CATEGORY(ThemeManagerLog, "theme_manager");
@@ -22,13 +23,23 @@ class ThemeManager : public QObject
public:
ThemeManager(QObject *parent = nullptr);
enum Role
{
MinRole = 0,
Hand = MinRole,
Stack,
Table,
Player,
MaxRole = Player,
};
private:
QBrush handBgBrush, stackBgBrush, tableBgBrush, playerBgBrush;
std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes;
/*
Internal cache for multiple backgrounds
*/
QBrushMap tableBgBrushesCache, stackBgBrushesCache, playerBgBrushesCache, handBgBrushesCache;
std::array<QBrushMap, Role::MaxRole + 1> brushesCache;
protected:
void ensureThemeDirectoryExists();
@@ -36,27 +47,10 @@ protected:
QBrush loadExtraBrush(QString fileName, QBrush &fallbackBrush);
public:
QBrush &getHandBgBrush()
{
return handBgBrush;
}
QBrush &getStackBgBrush()
{
return stackBgBrush;
}
QBrush &getTableBgBrush()
{
return tableBgBrush;
}
QBrush &getPlayerBgBrush()
{
return playerBgBrush;
}
QStringMap &getAvailableThemes();
QBrush getExtraTableBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraStackBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraPlayerBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush getExtraHandBgBrush(QString extraNumber, QBrush &fallbackBrush);
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
protected slots:
void themeChangedSlot();
signals:

View File

@@ -0,0 +1,125 @@
#include "color_identity_widget.h"
#include "../../../../../settings/cache_settings.h"
#include "mana_symbol_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QRegularExpression>
#include <QResizeEvent>
#include <QSize>
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);
// Define the full WUBRG set (White, Blue, Black, Red, Green)
QString fullColorIdentity = "WUBRG";
if (card) {
manaCost = card->getColors(); // Get mana cost string
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
populateManaSymbolWidgets();
}
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageDrawUnusedColorIdentitiesChanged, this,
&ColorIdentityWidget::toggleUnusedVisibility);
}
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, QString _manaCost)
: QWidget(parent), card(nullptr), manaCost(_manaCost)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);
populateManaSymbolWidgets();
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageDrawUnusedColorIdentitiesChanged, this,
&ColorIdentityWidget::toggleUnusedVisibility);
}
void ColorIdentityWidget::populateManaSymbolWidgets()
{
// Define the full WUBRG set (White, Blue, Black, Red, Green)
QString fullColorIdentity = "WUBRG";
QStringList symbols = parseColorIdentity(manaCost); // Parse mana cost string
if (SettingsCache::instance().getVisualDeckStorageDrawUnusedColorIdentities()) {
for (const QString symbol : fullColorIdentity) {
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol);
}
} else {
for (const QString &symbol : symbols) {
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol);
}
}
}
void ColorIdentityWidget::toggleUnusedVisibility()
{
if (layout != nullptr) {
QLayoutItem *item;
while ((item = layout->takeAt(0)) != nullptr) {
item->widget()->deleteLater(); // Delete the widget
delete item; // Delete the layout item
}
}
populateManaSymbolWidgets();
}
void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
setFixedHeight(totalHeight);
int spacing = layout->spacing();
int count = manaSymbols.size();
int availableWidth = totalWidth - (spacing * (count - 1));
int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}
QStringList ColorIdentityWidget::parseColorIdentity(const QString &cmc)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
}
return symbols;
}

View File

@@ -0,0 +1,29 @@
#ifndef COLOR_IDENTITY_WIDGET_H
#define COLOR_IDENTITY_WIDGET_H
#include "../../../../../game/cards/card_database.h"
#include <QHBoxLayout>
#include <QWidget>
class ColorIdentityWidget : public QWidget
{
Q_OBJECT
public:
explicit ColorIdentityWidget(QWidget *parent, CardInfoPtr card);
explicit ColorIdentityWidget(QWidget *parent, QString manaCost);
void populateManaSymbolWidgets();
QStringList parseColorIdentity(const QString &manaString);
public slots:
void resizeEvent(QResizeEvent *event) override;
void toggleUnusedVisibility();
private:
CardInfoPtr card;
QString manaCost;
QHBoxLayout *layout;
};
#endif // COLOR_IDENTITY_WIDGET_H

View File

@@ -0,0 +1,76 @@
#include "mana_cost_widget.h"
#include "mana_symbol_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>
#include <QSize>
#include <qregularexpression.h>
ManaCostWidget::ManaCostWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
setFixedHeight(50); // Fixed height
if (card) {
QString manaCost = card->getManaCost(); // Get mana cost string
QStringList symbols = parseManaCost(manaCost); // Parse mana cost string
for (const QString &symbol : symbols) {
ManaSymbolWidget *manaSymbol = new ManaSymbolWidget(this, symbol, true, false);
layout->addWidget(manaSymbol);
}
}
}
void ManaCostWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
if (!manaSymbols.isEmpty()) {
int totalWidth = event->size().width();
int spacing = layout->spacing();
int count = manaSymbols.size();
// Available width minus total spacing
int availableWidth = totalWidth - (spacing * (count - 1));
int iconSize = qMin(50, availableWidth / count);
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}
}
QStringList ManaCostWidget::parseManaCost(const QString &cmc)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
}
return symbols;
}

View File

@@ -0,0 +1,24 @@
#ifndef MANA_COST_WIDGET_H
#define MANA_COST_WIDGET_H
#include "../../../../../game/cards/card_database.h"
#include <QHBoxLayout>
#include <QWidget>
class ManaCostWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
QStringList parseManaCost(const QString &manaString);
public slots:
void resizeEvent(QResizeEvent *event) override;
private:
CardInfoPtr card;
QHBoxLayout *layout;
};
#endif // MANA_COST_WIDGET_H

View File

@@ -0,0 +1,70 @@
#include "mana_symbol_widget.h"
#include "../../../../../settings/cache_settings.h"
#include <QResizeEvent>
ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled)
: QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled)
{
loadManaIcon();
setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation));
setMaximumWidth(50);
// Initialize opacity effect
opacityEffect = new QGraphicsOpacityEffect(this);
setGraphicsEffect(opacityEffect);
updateOpacity();
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageUnusedColorIdentitiesOpacityChanged, this,
&ManaSymbolWidget::updateOpacity);
}
void ManaSymbolWidget::setColorActive(bool active)
{
if (isActive != active) {
isActive = active;
updateOpacity();
emit colorToggled(getSymbolChar(), isActive);
}
}
void ManaSymbolWidget::updateOpacity()
{
qreal opacity;
if (mayBeToggled) {
// UI elements that users can click on shouldn't be transparent.
opacity = isActive ? 1.0 : 0.5;
} else {
// It's just for display, they can do whatever they want.
opacity = isActive ? 1.0 : SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity() / 100.0;
}
opacityEffect->setOpacity(opacity);
}
void ManaSymbolWidget::mousePressEvent(QMouseEvent *event)
{
Q_UNUSED(event);
if (mayBeToggled) {
isActive = !isActive;
updateOpacity();
emit colorToggled(getSymbolChar(), isActive);
}
}
void ManaSymbolWidget::resizeEvent(QResizeEvent *event)
{
QLabel::resizeEvent(event);
setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void ManaSymbolWidget::loadManaIcon()
{
QString filename = "theme:icons/mana/";
if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") {
filename += symbol + ".svg";
}
manaIcon = QPixmap(filename);
}

View File

@@ -0,0 +1,43 @@
#ifndef MANA_SYMBOL_WIDGET_H
#define MANA_SYMBOL_WIDGET_H
#include <QGraphicsOpacityEffect>
#include <QLabel>
class ManaSymbolWidget : public QLabel
{
Q_OBJECT
public:
ManaSymbolWidget(QWidget *parent, QString symbol, bool isActive = true, bool mayBeToggled = false);
void setColorActive(bool active);
void updateOpacity();
bool isColorActive() const;
QString getSymbol() const
{
return symbol;
};
QChar getSymbolChar() const
{
return symbol[0];
};
void loadManaIcon();
public slots:
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
signals:
void colorToggled(QChar symbol, bool isActive);
private:
QString symbol;
QPixmap manaIcon;
bool isActive;
bool mayBeToggled;
QGraphicsOpacityEffect *opacityEffect;
};
#endif // MANA_SYMBOL_WIDGET_H

View File

@@ -1,5 +1,6 @@
#include "card_info_picture_enlarged_widget.h"
#include "../../../../settings/cache_settings.h"
#include "../../picture_loader/picture_loader.h"
#include <QPainterPath>
@@ -17,6 +18,12 @@ CardInfoPictureEnlargedWidget::CardInfoPictureEnlargedWidget(QWidget *parent)
{
setWindowFlags(Qt::ToolTip); // Keeps this widget on top of everything
setAttribute(Qt::WA_TranslucentBackground);
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
update();
});
}
/**
@@ -79,7 +86,8 @@ void CardInfoPictureEnlargedWidget::paintEvent(QPaintEvent *event)
QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
// Define the radius for rounded corners
qreal radius = 0.05 * scaledSize.width(); // Adjust the radius as needed for rounded corners
// Adjust the radius as needed for rounded corners
qreal radius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * scaledSize.width() : 0.;
QStylePainter painter(this);
// Fill the background with transparent color to ensure rounded corners are rendered properly

View File

@@ -3,7 +3,6 @@
#include "../../../../game/cards/card_database_manager.h"
#include "../../../../game/cards/card_item.h"
#include "../../../../settings/cache_settings.h"
#include "../../../tabs/tab_deck_editor.h"
#include "../../../tabs/tab_supervisor.h"
#include "../../picture_loader/picture_loader.h"
#include "../../window_main.h"
@@ -44,6 +43,12 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool hoverTo
hoverTimer = new QTimer(this);
hoverTimer->setSingleShot(true);
connect(hoverTimer, &QTimer::timeout, this, &CardInfoPictureWidget::showEnlargedPixmap);
connect(&SettingsCache::instance(), &SettingsCache::roundCardCornersChanged, this, [this](bool _roundCardCorners) {
Q_UNUSED(_roundCardCorners);
update();
});
}
/**
@@ -166,21 +171,37 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
}
}
// Adjust scaling after rotation
const QSize availableSize = size(); // Size of the widget
const QSize scaledSize = transformedPixmap.size().scaled(availableSize, Qt::KeepAspectRatio);
// Handle DPI scaling
qreal dpr = devicePixelRatio(); // Get the actual scaling factor
QSize availableSize = size() * dpr; // Convert to physical pixel size
const QRect targetRect{(availableSize.width() - scaledSize.width()) / 2,
(availableSize.height() - scaledSize.height()) / 2, scaledSize.width(), scaledSize.height()};
// Compute final scaled size
QSize pixmapSize = transformedPixmap.size();
QSize scaledSize = pixmapSize.scaled(availableSize, Qt::KeepAspectRatio);
const qreal radius = 0.05 * scaledSize.width();
// Pre-scale the pixmap once before drawing
QPixmap finalPixmap = transformedPixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
finalPixmap.setDevicePixelRatio(dpr); // Ensure correct display on high-DPI screens
// Compute target rectangle with explicit integer conversion
int targetX = static_cast<int>((availableSize.width() - scaledSize.width()) / (2 * dpr));
int targetY = static_cast<int>((availableSize.height() - scaledSize.height()) / (2 * dpr));
int targetW = static_cast<int>(scaledSize.width() / dpr);
int targetH = static_cast<int>(scaledSize.height() / dpr);
QRect targetRect{targetX, targetY, targetW, targetH};
// Compute rounded corner radius
// Ensure consistent rounding
qreal radius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * static_cast<qreal>(targetRect.width()) : 0.;
// Draw the pixmap with rounded corners
QStylePainter painter(this);
QPainterPath shape;
shape.addRoundedRect(targetRect, radius, radius);
painter.setClipPath(shape);
painter.drawPixmap(targetRect, transformedPixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
// Draw the pre-scaled pixmap directly
painter.drawPixmap(targetRect, finalPixmap);
}
/**
@@ -291,12 +312,27 @@ QMenu *CardInfoPictureWidget::createViewRelatedCardsMenu()
return viewRelatedCards;
}
/**
* Finds the single instance of the MainWindow in this application.
*/
static MainWindow *findMainWindow()
{
for (auto widget : QApplication::topLevelWidgets()) {
if (auto mainWindow = qobject_cast<MainWindow *>(widget)) {
return mainWindow;
}
}
// This code should be unreachable
qCritical() << "Could not find MainWindow in QApplication::topLevelWidgets";
return nullptr;
}
QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
{
auto addToOpenDeckMenu = new QMenu(tr("Add card to deck"));
auto *mainWindow = qobject_cast<MainWindow *>(window());
QList<TabDeckEditor *> deckEditorTabs = mainWindow->getTabSupervisor()->getDeckEditorTabs();
auto mainWindow = findMainWindow();
QList<AbstractTabDeckEditor *> deckEditorTabs = mainWindow->getTabSupervisor()->getDeckEditorTabs();
if (deckEditorTabs.isEmpty()) {
addToOpenDeckMenu->setEnabled(false);
@@ -308,14 +344,14 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu()
QAction *addCard = addCardMenu->addAction(tr("Mainboard"));
connect(addCard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCardInfo(info);
deckEditorTab->addCardHelper(info, DECK_ZONE_MAIN);
deckEditorTab->updateCard(info);
deckEditorTab->actAddCard(info);
});
QAction *addCardSideboard = addCardMenu->addAction(tr("Sideboard"));
connect(addCardSideboard, &QAction::triggered, this, [this, deckEditorTab] {
deckEditorTab->updateCardInfo(info);
deckEditorTab->addCardHelper(info, DECK_ZONE_SIDE);
deckEditorTab->updateCard(info);
deckEditorTab->actAddCardToSideboard(info);
});
}

View File

@@ -7,6 +7,8 @@
#include <QTimer>
#include <QWidget>
inline Q_LOGGING_CATEGORY(CardInfoPictureWidgetLog, "card_info_picture_widget");
class AbstractCardItem;
class QMenu;

View File

@@ -11,8 +11,8 @@
* This widget allows users to dynamically change the card size in a linked FlowWidget
* and updates the application's settings accordingly.
*/
CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *flowWidget, int defaultValue)
: parent(parent), flowWidget(flowWidget)
CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int defaultValue)
: parent(parent), flowWidget(_flowWidget)
{
cardSizeLayout = new QHBoxLayout(this);
cardSizeLayout->setContentsMargins(9, 0, 9, 0);

View File

@@ -38,6 +38,9 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
if (event->button() == Qt::LeftButton) {
lastMouseEvent = event;
singleClickTimer->start(QApplication::doubleClickInterval());
} else {
emit imageClicked(event, this);
event->accept();
}
}

View File

@@ -0,0 +1,46 @@
#include "deck_editor_card_info_dock_widget.h"
#include "../cards/card_info_frame_widget.h"
#include <QVBoxLayout>
DeckEditorCardInfoDockWidget::DeckEditorCardInfoDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent)
{
setObjectName("cardInfoDock");
setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable);
createCardInfoDock();
retranslateUi();
}
void DeckEditorCardInfoDockWidget::createCardInfoDock()
{
cardInfo = new CardInfoFrameWidget();
cardInfo->setObjectName("cardInfo");
auto *cardInfoFrame = new QVBoxLayout;
cardInfoFrame->setObjectName("cardInfoFrame");
cardInfoFrame->addWidget(cardInfo);
auto *cardInfoDockContents = new QWidget();
cardInfoDockContents->setObjectName("cardInfoDockContents");
cardInfoDockContents->setLayout(cardInfoFrame);
setWidget(cardInfoDockContents);
installEventFilter(deckEditor);
connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged);
}
void DeckEditorCardInfoDockWidget::updateCard(CardInfoPtr _card)
{
cardInfo->setCard(_card);
}
void DeckEditorCardInfoDockWidget::retranslateUi()
{
setWindowTitle(tr("Card Info"));
cardInfo->retranslateUi();
}

View File

@@ -0,0 +1,25 @@
#ifndef DECK_EDITOR_CARD_INFO_DOCK_WIDGET_H
#define DECK_EDITOR_CARD_INFO_DOCK_WIDGET_H
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "../cards/card_info_frame_widget.h"
#include <QDockWidget>
class AbstractTabDeckEditor;
class DeckEditorCardInfoDockWidget : public QDockWidget
{
Q_OBJECT
public:
explicit DeckEditorCardInfoDockWidget(AbstractTabDeckEditor *parent);
void createCardInfoDock();
void retranslateUi();
AbstractTabDeckEditor *deckEditor;
CardInfoFrameWidget *cardInfo;
public slots:
void updateCard(CardInfoPtr _card);
};
#endif // DECK_EDITOR_CARD_INFO_DOCK_WIDGET_H

View File

@@ -0,0 +1,282 @@
#include "deck_editor_database_display_widget.h"
#include "../../../../game/cards/card_database_manager.h"
#include "../../../../settings/cache_settings.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "../../../tabs/tab_supervisor.h"
#include "../../../ui/pixel_map_generator.h"
#include <QClipboard>
#include <QFile>
#include <QHeaderView>
#include <QMenu>
#include <QTextBrowser>
#include <QToolButton>
#include <QTreeView>
static bool canBeCommander(const CardInfoPtr &cardInfo)
{
return ((cardInfo->getCardType().contains("Legendary", Qt::CaseInsensitive) &&
cardInfo->getCardType().contains("Creature", Qt::CaseInsensitive))) ||
cardInfo->getText().contains("can be your commander", Qt::CaseInsensitive);
}
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent)
: QWidget(parent), deckEditor(parent)
{
setObjectName("centralWidget");
centralFrame = new QVBoxLayout(this);
centralFrame->setObjectName("centralFrame");
setLayout(centralFrame);
searchEdit = new SearchLineEdit();
searchEdit->setObjectName("searchEdit");
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
searchEdit->installEventFilter(&searchKeySignals);
setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus);
searchKeySignals.setObjectName("searchKeySignals");
connect(searchEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateSearch(const QString &)));
connect(&searchKeySignals, &KeySignals::onEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck);
connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this,
&DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this,
&DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(&searchKeySignals, &KeySignals::onCtrlC, this, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents);
connect(help, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::showSearchSyntaxHelp);
databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
databaseDisplayModel = new CardDatabaseDisplayModel(this);
databaseDisplayModel->setObjectName("databaseDisplayModel");
databaseDisplayModel->setSourceModel(databaseModel);
databaseDisplayModel->setFilterKeyColumn(0);
databaseView = new QTreeView(this);
databaseView->setObjectName("databaseView");
databaseView->setFocusProxy(searchEdit);
databaseView->setUniformRowHeights(true);
databaseView->setRootIsDecorated(false);
databaseView->setAlternatingRowColors(true);
databaseView->setSortingEnabled(true);
databaseView->sortByColumn(0, Qt::AscendingOrder);
databaseView->setModel(databaseDisplayModel);
databaseView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(databaseView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(databaseCustomMenu(QPoint)));
connect(databaseView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
&DeckEditorDatabaseDisplayWidget::updateCard);
connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState();
if (dbHeaderState.isNull()) {
// first run
databaseView->setColumnWidth(0, 200);
} else {
databaseView->header()->restoreState(dbHeaderState);
}
connect(databaseView->header(), SIGNAL(geometriesChanged()), this, SLOT(saveDbHeaderState()));
searchEdit->setTreeView(databaseView);
aAddCard = new QAction(QString(), this);
aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
auto *tbAddCard = new QToolButton(this);
tbAddCard->setDefaultAction(aAddCard);
aAddCardToSideboard = new QAction(QString(), this);
aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue"));
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
auto *tbAddCardToSideboard = new QToolButton(this);
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
searchLayout = new QHBoxLayout;
searchLayout->setObjectName("searchLayout");
searchLayout->addWidget(searchEdit);
searchLayout->addWidget(tbAddCard);
searchLayout->addWidget(tbAddCardToSideboard);
centralFrame->addLayout(searchLayout);
centralFrame->addWidget(databaseView);
retranslateUi();
}
void DeckEditorDatabaseDisplayWidget::updateSearch(const QString &search)
{
databaseDisplayModel->setStringFilter(search);
QModelIndexList sel = databaseView->selectionModel()->selectedRows();
if (sel.isEmpty() && databaseDisplayModel->rowCount())
databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0),
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters()
{
databaseDisplayModel->clearFilterAll();
searchEdit->setText("");
}
void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex &current, const QModelIndex & /*previous*/)
{
const QString cardName = current.sibling(current.row(), 0).data().toString();
const QString cardProviderID = CardDatabaseManager::getInstance()->getPreferredPrintingProviderIdForCard(cardName);
if (!current.isValid()) {
return;
}
if (!current.model()->hasChildren(current.sibling(current.row(), 0))) {
CardInfoPtr card = CardDatabaseManager::getInstance()->getCardByNameAndProviderId(cardName, cardProviderID);
if (!card) {
card = CardDatabaseManager::getInstance()->getCard(cardName);
}
emit cardChanged(card);
}
}
void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck()
{
emit addCardToMainDeck(currentCardInfo());
}
void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard()
{
emit addCardToSideboard(currentCardInfo());
}
void DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck()
{
emit decrementCardFromMainDeck(currentCardInfo());
}
void DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard()
{
emit decrementCardFromSideboard(currentCardInfo());
}
CardInfoPtr DeckEditorDatabaseDisplayWidget::currentCardInfo() const
{
const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex();
if (!currentIndex.isValid()) {
return {};
}
const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString();
return CardDatabaseManager::getInstance()->getCard(cardName);
}
void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point)
{
QMenu menu;
const CardInfoPtr info = currentCardInfo();
if (info) {
// add to deck and sideboard options
QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard;
addToDeck = menu.addAction(tr("Add to Deck"));
addToSideboard = menu.addAction(tr("Add to Sideboard"));
selectPrinting = menu.addAction(tr("Select Printing"));
if (canBeCommander(info)) {
edhRecCommander = menu.addAction(tr("Show on EDHREC (Commander)"));
connect(edhRecCommander, &QAction::triggered, this,
[this, info] { deckEditor->getTabSupervisor()->addEdhrecTab(info, true); });
}
edhRecCard = menu.addAction(tr("Show on EDHREC (Card)"));
connect(addToDeck, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
connect(addToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
connect(selectPrinting, &QAction::triggered, this, [this, info] { deckEditor->showPrintingSelector(); });
connect(edhRecCard, &QAction::triggered, this,
[this, info] { deckEditor->getTabSupervisor()->addEdhrecTab(info); });
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = info->getAllRelatedCards();
if (relatedCards.isEmpty()) {
relatedMenu->setDisabled(true);
} else {
for (const CardRelation *rel : relatedCards) {
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(
relatedCard, &QAction::triggered, deckEditor->cardInfoDockWidget->cardInfo,
[this, relatedCardName] { deckEditor->cardInfoDockWidget->cardInfo->setCard(relatedCardName); });
}
}
menu.exec(databaseView->mapToGlobal(point));
}
}
void DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents()
{
auto _data = databaseView->selectionModel()->currentIndex().data();
QApplication::clipboard()->setText(_data.toString());
}
void DeckEditorDatabaseDisplayWidget::saveDbHeaderState()
{
SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState());
}
void DeckEditorDatabaseDisplayWidget::showSearchSyntaxHelp()
{
QFile file("theme:help/search.md");
if (!file.open(QFile::ReadOnly | QFile::Text)) {
return;
}
QTextStream in(&file);
QString text = in.readAll();
file.close();
// Poor Markdown Converter
auto opts = QRegularExpression::MultilineOption;
text = text.replace(QRegularExpression("^(###)(.*)", opts), "<h3>\\2</h3>")
.replace(QRegularExpression("^(##)(.*)", opts), "<h2>\\2</h2>")
.replace(QRegularExpression("^(#)(.*)", opts), "<h1>\\2</h1>")
.replace(QRegularExpression("^------*", opts), "<hr />")
.replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(<a href='\2'>\1</a>)");
auto browser = new QTextBrowser;
browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint |
Qt::WindowCloseButtonHint | Qt::WindowFullscreenButtonHint);
browser->setWindowTitle("Search Help");
browser->setReadOnly(true);
browser->setMinimumSize({500, 600});
QString sheet = QString("a { text-decoration: underline; color: rgb(71,158,252) };");
browser->document()->setDefaultStyleSheet(sheet);
browser->setHtml(text);
connect(browser, &QTextBrowser::anchorClicked, [this](const QUrl &link) { searchEdit->setText(link.fragment()); });
browser->show();
}
void DeckEditorDatabaseDisplayWidget::setFilterTree(FilterTree *filterTree)
{
databaseDisplayModel->setFilterTree(filterTree);
}
void DeckEditorDatabaseDisplayWidget::retranslateUi()
{
aAddCard->setText(tr("Add card to &maindeck"));
aAddCardToSideboard->setText(tr("Add card to &sideboard"));
}

View File

@@ -0,0 +1,58 @@
#ifndef DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H
#define DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H
#include "../../../../deck/custom_line_edit.h"
#include "../../../../game/cards/card_database_model.h"
#include "../../../game_logic/key_signals.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include <QHBoxLayout>
#include <QWidget>
class AbstractTabDeckEditor;
class DeckEditorDatabaseDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent);
AbstractTabDeckEditor *deckEditor;
SearchLineEdit *searchEdit;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
public slots:
CardInfoPtr currentCardInfo() const;
void setFilterTree(FilterTree *filterTree);
void clearAllDatabaseFilters();
signals:
void addCardToMainDeck(CardInfoPtr card);
void addCardToSideboard(CardInfoPtr card);
void decrementCardFromMainDeck(CardInfoPtr card);
void decrementCardFromSideboard(CardInfoPtr card);
void cardChanged(CardInfoPtr _card);
private:
KeySignals searchKeySignals;
QTreeView *databaseView;
QHBoxLayout *searchLayout;
QAction *aAddCard, *aAddCardToSideboard;
QVBoxLayout *centralFrame;
QWidget *centralWidget;
private slots:
void showSearchSyntaxHelp();
void retranslateUi();
void updateSearch(const QString &search);
void updateCard(const QModelIndex &current, const QModelIndex &);
void actAddCardToMainDeck();
void actAddCardToSideboard();
void actDecrementCardFromMainDeck();
void actDecrementCardFromSideboard();
void databaseCustomMenu(QPoint point);
void copyDatabaseCellContents();
void saveDbHeaderState();
};
#endif // DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H

View File

@@ -0,0 +1,533 @@
#include "deck_editor_deck_dock_widget.h"
#include "../../../../game/cards/card_database_manager.h"
#include "../../../../settings/cache_settings.h"
#include <QComboBox>
#include <QDockWidget>
#include <QHeaderView>
#include <QLabel>
#include <QSplitter>
#include <QTextEdit>
#include <trice_limits.h>
DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent)
{
setObjectName("deckDock");
setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable);
installEventFilter(deckEditor);
connect(this, SIGNAL(topLevelChanged(bool)), deckEditor, SLOT(dockTopLevelChanged(bool)));
createDeckDock();
}
void DeckEditorDeckDockWidget::createDeckDock()
{
deckModel = new DeckListModel(this);
deckModel->setObjectName("deckModel");
connect(deckModel, SIGNAL(deckHashChanged()), this, SLOT(updateHash()));
deckView = new QTreeView();
deckView->setObjectName("deckView");
deckView->setModel(deckModel);
deckView->setUniformRowHeights(true);
deckView->setSortingEnabled(true);
deckView->sortByColumn(1, Qt::AscendingOrder);
deckView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
deckView->installEventFilter(&deckViewKeySignals);
deckView->setContextMenuPolicy(Qt::CustomContextMenu);
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(deckView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
&DeckEditorDeckDockWidget::updateCard);
connect(deckView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actSwapCard()));
connect(deckView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(decklistCustomMenu(QPoint)));
connect(&deckViewKeySignals, SIGNAL(onShiftS()), this, SLOT(actSwapCard()));
connect(&deckViewKeySignals, SIGNAL(onEnter()), this, SLOT(actIncrement()));
connect(&deckViewKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actIncrement()));
connect(&deckViewKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrementSelection()));
connect(&deckViewKeySignals, SIGNAL(onShiftRight()), this, SLOT(actIncrement()));
connect(&deckViewKeySignals, SIGNAL(onShiftLeft()), this, SLOT(actDecrementSelection()));
connect(&deckViewKeySignals, SIGNAL(onDelete()), this, SLOT(actRemoveCard()));
nameLabel = new QLabel();
nameLabel->setObjectName("nameLabel");
nameEdit = new LineEditUnfocusable;
nameEdit->setMaxLength(MAX_NAME_LENGTH);
nameEdit->setObjectName("nameEdit");
nameLabel->setBuddy(nameEdit);
connect(nameEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateName(const QString &)));
commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit;
commentsEdit->setAcceptRichText(false);
commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height());
commentsEdit->setObjectName("commentsEdit");
commentsLabel->setBuddy(commentsEdit);
connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments()));
bannerCardLabel = new QLabel();
bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardLabel->setText(tr("Banner Card"));
bannerCardComboBox = new QComboBox(this);
connect(deckModel, &DeckListModel::dataChanged, this, [this]() {
// Delay the update to avoid race conditions
QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox);
});
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&DeckEditorDeckDockWidget::setBannerCard);
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment"));
connect(aIncrement, SIGNAL(triggered()), this, SLOT(actIncrement()));
auto *tbIncrement = new QToolButton(this);
tbIncrement->setDefaultAction(aIncrement);
aDecrement = new QAction(QString(), this);
aDecrement->setIcon(QPixmap("theme:icons/decrement"));
connect(aDecrement, SIGNAL(triggered()), this, SLOT(actDecrementSelection()));
auto *tbDecrement = new QToolButton(this);
tbDecrement->setDefaultAction(aDecrement);
aRemoveCard = new QAction(QString(), this);
aRemoveCard->setIcon(QPixmap("theme:icons/remove_row"));
connect(aRemoveCard, SIGNAL(triggered()), this, SLOT(actRemoveCard()));
auto *tbRemoveCard = new QToolButton(this);
tbRemoveCard->setDefaultAction(aRemoveCard);
aSwapCard = new QAction(QString(), this);
aSwapCard->setIcon(QPixmap("theme:icons/swap"));
connect(aSwapCard, SIGNAL(triggered()), this, SLOT(actSwapCard()));
auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard);
auto *upperLayout = new QGridLayout;
upperLayout->setObjectName("upperLayout");
upperLayout->setContentsMargins(11, 11, 11, 0);
upperLayout->addWidget(nameLabel, 0, 0);
upperLayout->addWidget(nameEdit, 0, 1);
upperLayout->addWidget(commentsLabel, 1, 0);
upperLayout->addWidget(commentsEdit, 1, 1);
upperLayout->addWidget(bannerCardLabel, 2, 0);
upperLayout->addWidget(bannerCardComboBox, 2, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1");
auto *hashSizePolicy = new QSizePolicy();
hashSizePolicy->setHorizontalPolicy(QSizePolicy::Fixed);
hashLabel1->setSizePolicy(*hashSizePolicy);
hashLabel = new LineEditUnfocusable;
hashLabel->setObjectName("hashLabel");
hashLabel->setReadOnly(true);
hashLabel->setFrame(false);
auto *lowerLayout = new QGridLayout;
lowerLayout->setObjectName("lowerLayout");
lowerLayout->addWidget(hashLabel1, 0, 0);
lowerLayout->addWidget(hashLabel, 0, 1);
lowerLayout->addWidget(tbIncrement, 0, 2);
lowerLayout->addWidget(tbDecrement, 0, 3);
lowerLayout->addWidget(tbRemoveCard, 0, 4);
lowerLayout->addWidget(tbSwapCard, 0, 5);
lowerLayout->addWidget(deckView, 1, 0, 1, 6);
// Create widgets for both layouts to make splitter work correctly
auto *topWidget = new QWidget;
topWidget->setLayout(upperLayout);
auto *bottomWidget = new QWidget;
bottomWidget->setLayout(lowerLayout);
auto *split = new QSplitter;
split->setObjectName("deckSplitter");
split->setOrientation(Qt::Vertical);
split->setChildrenCollapsible(true);
split->addWidget(topWidget);
split->addWidget(bottomWidget);
split->setStretchFactor(0, 1);
split->setStretchFactor(1, 4);
auto *rightFrame = new QVBoxLayout;
rightFrame->setObjectName("rightFrame");
rightFrame->addWidget(split);
auto *deckDockContents = new QWidget();
deckDockContents->setObjectName("deckDockContents");
deckDockContents->setLayout(rightFrame);
setWidget(deckDockContents);
refreshShortcuts();
retranslateUi();
}
CardInfoPtr DeckEditorDeckDockWidget::getCurrentCard()
{
QModelIndex current = deckView->selectionModel()->currentIndex();
if (!current.isValid())
return {};
const QString cardName = current.sibling(current.row(), 1).data().toString();
const QString cardProviderID = current.sibling(current.row(), 4).data().toString();
const QModelIndex gparent = current.parent().parent();
if (!gparent.isValid()) {
return {};
}
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
if (!current.model()->hasChildren(current.sibling(current.row(), 0))) {
QString cardName = current.sibling(current.row(), 1).data().toString();
QString providerId = current.sibling(current.row(), 4).data().toString();
if (CardInfoPtr selectedCard =
CardDatabaseManager::getInstance()->getCardByNameAndProviderId(cardName, providerId)) {
return selectedCard;
}
}
return {};
}
void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*&current*/, const QModelIndex & /*previous*/)
{
if (CardInfoPtr card = getCurrentCard()) {
emit cardChanged(card);
}
}
void DeckEditorDeckDockWidget::updateName(const QString &name)
{
deckModel->getDeckList()->setName(name);
emit deckChanged();
}
void DeckEditorDeckDockWidget::updateComments()
{
deckModel->getDeckList()->setComments(commentsEdit->toPlainText());
emit deckChanged();
}
void DeckEditorDeckDockWidget::updateHash()
{
hashLabel->setText(deckModel->getDeckList()->getDeckHash());
emit deckChanged();
}
void DeckEditorDeckDockWidget::updateBannerCardComboBox()
{
// Store the current text of the combo box
QString currentText = bannerCardComboBox->currentText();
// Block signals temporarily
bool wasBlocked = bannerCardComboBox->blockSignals(true);
// Clear the existing items in the combo box
bannerCardComboBox->clear();
// Prepare the new items with deduplication
QSet<QPair<QString, QString>> bannerCardSet;
InnerDecklistNode *listRoot = deckModel->getDeckList()->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
CardInfoPtr info = CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
currentCard->getName(), currentCard->getCardProviderId());
if (info) {
bannerCardSet.insert(
QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
}
}
}
}
QList<QPair<QString, QString>> pairList = bannerCardSet.values();
// Sort QList by the first() element of the QPair
std::sort(pairList.begin(), pairList.end(), [](const QPair<QString, QString> &a, const QPair<QString, QString> &b) {
return a.first.toLower() < b.first.toLower();
});
for (const auto &pair : pairList) {
QVariantMap dataMap;
dataMap["name"] = pair.first;
dataMap["uuid"] = pair.second;
bannerCardComboBox->addItem(pair.first, dataMap);
}
// Try to restore the previous selection by finding the currentText
int restoredIndex = bannerCardComboBox->findText(currentText);
if (restoredIndex != -1) {
bannerCardComboBox->setCurrentIndex(restoredIndex);
} else {
// Add a placeholder "-" and set it as the current selection
int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard().first);
if (bannerIndex != -1) {
bannerCardComboBox->setCurrentIndex(bannerIndex);
} else {
bannerCardComboBox->insertItem(0, "-");
bannerCardComboBox->setCurrentIndex(0);
}
}
// Restore the previous signal blocking state
bannerCardComboBox->blockSignals(wasBlocked);
}
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
{
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
deckModel->getDeckList()->setBannerCard(
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
emit deckChanged();
}
/**
* Sets the currently active deck for this tab
* @param _deck The deck. Takes ownership of the object
*/
void DeckEditorDeckDockWidget::setDeck(DeckLoader *_deck)
{
deckModel->setDeckList(_deck);
nameEdit->setText(deckModel->getDeckList()->getName());
commentsEdit->setText(deckModel->getDeckList()->getComments());
bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().first);
updateBannerCardComboBox();
updateHash();
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckView->expandAll();
deckView->expandAll();
deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList());
}
DeckLoader *DeckEditorDeckDockWidget::getDeckList()
{
return deckModel->getDeckList();
}
/**
* Resets the tab to the state for a blank new tab.
*/
void DeckEditorDeckDockWidget::cleanDeck()
{
deckModel->cleanList();
nameEdit->setText(QString());
commentsEdit->setText(QString());
hashLabel->setText(QString());
updateBannerCardComboBox();
deckTagsDisplayWidget->connectDeckList(deckModel->getDeckList());
}
void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index)
{
if (index.parent().isValid())
recursiveExpand(index.parent());
deckView->expand(index);
}
/**
* Gets the index of all the currently selected card nodes in the decklist table.
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
*
* @return A model index list containing all selected card nodes
*/
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const
{
auto selectedRows = deckView->selectionModel()->selectedRows();
const auto notLeafNode = [this](const auto &index) { return deckModel->hasChildren(index); };
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end());
return selectedRows;
}
void DeckEditorDeckDockWidget::actIncrement()
{
auto selectedRows = getSelectedCardNodes();
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, 1);
}
}
void DeckEditorDeckDockWidget::actSwapCard()
{
auto selectedRows = getSelectedCardNodes();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
bool isModified = false;
for (const auto &currentIndex : selectedRows) {
if (swapCard(currentIndex)) {
isModified = true;
}
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
emit deckChanged();
}
update();
}
/**
* Swaps the card at the index between the maindeck and sideboard
*
* @param currentIndex The index to swap.
* @return True if the swap was successful
*/
bool DeckEditorDeckDockWidget::swapCard(const QModelIndex &currentIndex)
{
if (!currentIndex.isValid())
return false;
const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString();
const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 4).data().toString();
const QModelIndex gparent = currentIndex.parent().parent();
if (!gparent.isValid())
return false;
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
offsetCountAtIndex(currentIndex, -1);
const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
// Third argument (true) says create the card no matter what, even if not in DB
QModelIndex newCardIndex = deckModel->addCard(
cardName, CardDatabaseManager::getInstance()->getSpecificSetForCard(cardName, cardProviderID), otherZoneName,
true);
recursiveExpand(newCardIndex);
return true;
}
void DeckEditorDeckDockWidget::actDecrementCard(CardInfoPtr info, QString zoneName)
{
if (!info)
return;
if (info->getIsToken())
zoneName = DECK_ZONE_TOKENS;
QString providerId = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("uuid");
QString collectorNumber = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("num");
QModelIndex idx = deckModel->findCard(info->getName(), zoneName, providerId, collectorNumber);
if (!idx.isValid()) {
return;
}
deckView->clearSelection();
deckView->setCurrentIndex(idx);
offsetCountAtIndex(idx, -1);
}
void DeckEditorDeckDockWidget::actDecrementSelection()
{
auto selectedRows = getSelectedCardNodes();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, -1);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
void DeckEditorDeckDockWidget::actRemoveCard()
{
auto selectedRows = getSelectedCardNodes();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
bool isModified = false;
for (const auto &index : selectedRows) {
if (!index.isValid() || deckModel->hasChildren(index)) {
continue;
}
deckModel->removeRow(index.row(), index.parent());
isModified = true;
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
emit deckChanged();
}
}
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int offset)
{
if (!idx.isValid() || deckModel->hasChildren(idx)) {
return;
}
const QModelIndex numberIndex = idx.sibling(idx.row(), 0);
const int count = deckModel->data(numberIndex, Qt::EditRole).toInt();
const int new_count = count + offset;
if (new_count <= 0)
deckModel->removeRow(idx.row(), idx.parent());
else
deckModel->setData(numberIndex, new_count, Qt::EditRole);
emit deckChanged();
}
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)
{
QMenu menu;
const CardInfoPtr info = getCurrentCard();
QAction *selectPrinting = menu.addAction(tr("Select Printing"));
connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector);
menu.exec(deckView->mapToGlobal(point));
}
void DeckEditorDeckDockWidget::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
aRemoveCard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aRemoveCard"));
aIncrement->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aIncrement"));
aDecrement->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aDecrement"));
}
void DeckEditorDeckDockWidget::retranslateUi()
{
setWindowTitle(tr("Deck"));
nameLabel->setText(tr("Deck &name:"));
commentsLabel->setText(tr("&Comments:"));
hashLabel1->setText(tr("Hash:"));
aIncrement->setText(tr("&Increment number"));
aDecrement->setText(tr("&Decrement number"));
aRemoveCard->setText(tr("&Remove row"));
aSwapCard->setText(tr("Swap card to/from sideboard"));
}

View File

@@ -0,0 +1,82 @@
#ifndef DECK_EDITOR_DECK_DOCK_WIDGET_H
#define DECK_EDITOR_DECK_DOCK_WIDGET_H
#include "../../../../deck/custom_line_edit.h"
#include "../../../../game/cards/card_database.h"
#include "../../../game_logic/key_signals.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include <QComboBox>
#include <QDockWidget>
#include <QLabel>
#include <QTextEdit>
#include <QTreeView>
class DeckListModel;
class AbstractTabDeckEditor;
class DeckEditorDeckDockWidget : public QDockWidget
{
Q_OBJECT
public:
explicit DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent);
DeckListModel *deckModel;
QTreeView *deckView;
QComboBox *bannerCardComboBox;
void createDeckDock();
CardInfoPtr getCurrentCard();
void retranslateUi();
QString getDeckName()
{
return nameEdit->text();
}
QString getSimpleDeckName()
{
return nameEdit->text().simplified();
}
public slots:
void cleanDeck();
void updateBannerCardComboBox();
void setDeck(DeckLoader *_deck);
DeckLoader *getDeckList();
void actIncrement();
bool swapCard(const QModelIndex &idx);
void actDecrementCard(CardInfoPtr info, QString zoneName);
void actDecrementSelection();
void actSwapCard();
void actRemoveCard();
void offsetCountAtIndex(const QModelIndex &idx, int offset);
signals:
void deckChanged();
void cardChanged(CardInfoPtr _card);
private:
AbstractTabDeckEditor *deckEditor;
KeySignals deckViewKeySignals;
QLabel *nameLabel;
LineEditUnfocusable *nameEdit;
QLabel *commentsLabel;
QTextEdit *commentsEdit;
QLabel *bannerCardLabel;
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
QLabel *hashLabel1;
LineEditUnfocusable *hashLabel;
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
void recursiveExpand(const QModelIndex &index);
QModelIndexList getSelectedCardNodes() const;
private slots:
void decklistCustomMenu(QPoint point);
void updateCard(QModelIndex, const QModelIndex &current);
void updateName(const QString &name);
void updateComments();
void setBannerCard(int);
void updateHash();
void refreshShortcuts();
};
#endif // DECK_EDITOR_DECK_DOCK_WIDGET_H

View File

@@ -0,0 +1,143 @@
#include "deck_editor_filter_dock_widget.h"
#include "../../../../game/cards/card_database_model.h"
#include "../../../../game/filters/filter_builder.h"
#include "../../../../game/filters/filter_tree_model.h"
#include "../../../../settings/cache_settings.h"
#include <QGridLayout>
#include <QMenu>
#include <QToolButton>
DeckEditorFilterDockWidget::DeckEditorFilterDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent)
{
setObjectName("filterDock");
setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable);
createFiltersDock();
retranslateUi();
}
void DeckEditorFilterDockWidget::createFiltersDock()
{
filterModel = new FilterTreeModel();
filterModel->setObjectName("filterModel");
deckEditor->filterTreeChanged(filterModel->filterTree());
filterView = new QTreeView;
filterView->setObjectName("filterView");
filterView->setModel(filterModel);
filterView->setUniformRowHeights(true);
filterView->setHeaderHidden(true);
filterView->setContextMenuPolicy(Qt::CustomContextMenu);
filterView->installEventFilter(&filterViewKeySignals);
connect(filterModel, SIGNAL(layoutChanged()), filterView, SLOT(expandAll()));
connect(filterView, SIGNAL(customContextMenuRequested(const QPoint &)), this,
SLOT(filterViewCustomContextMenu(const QPoint &)));
connect(&filterViewKeySignals, SIGNAL(onDelete()), this, SLOT(actClearFilterOne()));
auto *filterBuilder = new FilterBuilder;
filterBuilder->setObjectName("filterBuilder");
connect(filterBuilder, SIGNAL(add(const CardFilter *)), filterModel, SLOT(addFilter(const CardFilter *)));
aClearFilterOne = new QAction(QString(), this);
aClearFilterOne->setIcon(QPixmap("theme:icons/decrement"));
connect(aClearFilterOne, SIGNAL(triggered()), this, SLOT(actClearFilterOne()));
aClearFilterAll = new QAction(QString(), this);
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
connect(aClearFilterAll, SIGNAL(triggered()), this, SLOT(actClearFilterAll()));
auto *filterDelOne = new QToolButton();
filterDelOne->setObjectName("filterDelOne");
filterDelOne->setDefaultAction(aClearFilterOne);
filterDelOne->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
auto *filterDelAll = new QToolButton();
filterDelAll->setObjectName("filterDelAll");
filterDelAll->setDefaultAction(aClearFilterAll);
filterDelAll->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
auto *filterLayout = new QGridLayout;
filterLayout->setObjectName("filterLayout");
filterLayout->setContentsMargins(0, 0, 0, 0);
filterLayout->addWidget(filterBuilder, 0, 0, 1, 3);
filterLayout->addWidget(filterView, 1, 0, 1, 3);
filterLayout->addWidget(filterDelOne, 2, 0, 1, 1);
filterLayout->addWidget(filterDelAll, 2, 2, 1, 1);
filterBox = new QWidget();
filterBox->setObjectName("filterBox");
filterBox->setLayout(filterLayout);
auto *filterFrame = new QVBoxLayout;
filterFrame->setObjectName("filterFrame");
filterFrame->addWidget(filterBox);
auto *filterDockContents = new QWidget(this);
filterDockContents->setObjectName("filterDockContents");
filterDockContents->setLayout(filterFrame);
setWidget(filterDockContents);
installEventFilter(deckEditor);
connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged);
}
void DeckEditorFilterDockWidget::filterViewCustomContextMenu(const QPoint &point)
{
QMenu menu;
QAction *action;
QModelIndex idx;
idx = filterView->indexAt(point);
if (!idx.isValid())
return;
action = menu.addAction(QString("delete"));
action->setData(point);
connect(&menu, &QMenu::triggered, this, &DeckEditorFilterDockWidget::filterRemove);
menu.exec(filterView->mapToGlobal(point));
}
void DeckEditorFilterDockWidget::filterRemove(const QAction *action)
{
QPoint point;
QModelIndex idx;
point = action->data().toPoint();
idx = filterView->indexAt(point);
if (!idx.isValid())
return;
filterModel->removeRow(idx.row(), idx.parent());
}
void DeckEditorFilterDockWidget::actClearFilterAll()
{
emit clearAllDatabaseFilters();
}
void DeckEditorFilterDockWidget::actClearFilterOne()
{
QModelIndexList selIndexes = filterView->selectionModel()->selectedIndexes();
for (QModelIndex idx : selIndexes) {
filterModel->removeRow(idx.row(), idx.parent());
}
}
void DeckEditorFilterDockWidget::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
aClearFilterAll->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClearFilterAll"));
aClearFilterOne->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClearFilterOne"));
}
void DeckEditorFilterDockWidget::retranslateUi()
{
setWindowTitle(tr("Filters"));
aClearFilterAll->setText(tr("&Clear all filters"));
aClearFilterOne->setText(tr("Delete selected"));
}

View File

@@ -0,0 +1,39 @@
#ifndef DECK_EDITOR_FILTER_DOCK_WIDGET_H
#define DECK_EDITOR_FILTER_DOCK_WIDGET_H
#include "../../../game_logic/key_signals.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include <QDockWidget>
#include <QTreeView>
class FilterTreeModel;
class AbstractTabDeckEditor;
class DeckEditorFilterDockWidget : public QDockWidget
{
Q_OBJECT
public:
explicit DeckEditorFilterDockWidget(AbstractTabDeckEditor *parent);
void createFiltersDock();
void retranslateUi();
QAction *aClearFilterAll, *aClearFilterOne;
signals:
void clearAllDatabaseFilters();
private:
AbstractTabDeckEditor *deckEditor;
FilterTreeModel *filterModel;
QTreeView *filterView;
KeySignals filterViewKeySignals;
QWidget *filterBox;
private slots:
void filterViewCustomContextMenu(const QPoint &point);
void filterRemove(const QAction *action);
void actClearFilterAll();
void actClearFilterOne();
void refreshShortcuts();
};
#endif // DECK_EDITOR_FILTER_DOCK_WIDGET_H

View File

@@ -0,0 +1,41 @@
#include "deck_editor_printing_selector_dock_widget.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include <QVBoxLayout>
DeckEditorPrintingSelectorDockWidget::DeckEditorPrintingSelectorDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent)
{
setObjectName("printingSelectorDock");
setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable);
setFloating(false);
createPrintingSelectorDock();
retranslateUi();
}
void DeckEditorPrintingSelectorDockWidget::createPrintingSelectorDock()
{
printingSelector = new PrintingSelector(this, deckEditor);
printingSelector->setObjectName("printingSelector");
auto *printingSelectorFrame = new QVBoxLayout;
printingSelectorFrame->setObjectName("printingSelectorFrame");
printingSelectorFrame->addWidget(printingSelector);
auto *printingSelectorDockContents = new QWidget();
printingSelectorDockContents->setObjectName("printingSelectorDockContents");
printingSelectorDockContents->setLayout(printingSelectorFrame);
setWidget(printingSelectorDockContents);
installEventFilter(deckEditor);
connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged);
}
void DeckEditorPrintingSelectorDockWidget::retranslateUi()
{
setWindowTitle(tr("Printing Selector"));
}

View File

@@ -0,0 +1,23 @@
#ifndef DECK_EDITOR_PRINTING_SELECTOR_DOCK_WIDGET_H
#define DECK_EDITOR_PRINTING_SELECTOR_DOCK_WIDGET_H
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "../printing_selector/printing_selector.h"
#include <QDockWidget>
class TabDeckEditor;
class DeckEditorPrintingSelectorDockWidget : public QDockWidget
{
Q_OBJECT
public:
explicit DeckEditorPrintingSelectorDockWidget(AbstractTabDeckEditor *parent);
void createPrintingSelectorDock();
void retranslateUi();
PrintingSelector *printingSelector;
private:
AbstractTabDeckEditor *deckEditor;
};
#endif // DECK_EDITOR_PRINTING_SELECTOR_DOCK_WIDGET_H

View File

@@ -1,5 +1,7 @@
#include "banner_widget.h"
#include "../../../../../client/ui/pixel_map_generator.h"
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
@@ -8,19 +10,26 @@
BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation orientation, int transparency)
: QWidget(parent), gradientOrientation(orientation), transparency(qBound(0, transparency, 100))
{
auto layout = new QHBoxLayout(this);
iconLabel = new QLabel(this);
iconLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
// Create the banner label and set properties
bannerLabel = new QLabel(text, this);
bannerLabel->setAlignment(Qt::AlignCenter);
bannerLabel->setStyleSheet("font-size: 24px; font-weight: bold; color: white;");
// Layout to center the banner label
layout = new QVBoxLayout(this);
layout->addWidget(iconLabel);
layout->addWidget(bannerLabel);
layout->addWidget(new QLabel(this)); // add dummy label to force text label to be centered
setLayout(layout);
// Set minimum height for the widget
setMinimumHeight(50);
connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility);
updateDropdownIconState();
}
void BannerWidget::mousePressEvent(QMouseEvent *event)
@@ -36,10 +45,33 @@ void BannerWidget::setText(const QString &text) const
bannerLabel->setText(text);
}
void BannerWidget::setClickable(bool _clickable)
{
clickable = _clickable;
updateDropdownIconState();
}
void BannerWidget::setBuddy(QWidget *_buddy)
{
buddy = _buddy;
updateDropdownIconState();
}
void BannerWidget::toggleBuddyVisibility() const
{
if (buddy) {
buddy->setVisible(!buddy->isVisible());
updateDropdownIconState();
}
}
void BannerWidget::updateDropdownIconState() const
{
if (clickable && buddy) {
iconLabel->setPixmap(DropdownIconPixmapGenerator::generatePixmap(24, !buddy->isHidden()));
} else {
// we cannot directly hide the iconLabel, since it's needed to center the text; set an empty image instead
iconLabel->setPixmap(QPixmap());
}
}

View File

@@ -16,33 +16,28 @@ public:
int transparency = 80);
void mousePressEvent(QMouseEvent *event) override;
void setText(const QString &text) const;
void setBuddy(QWidget *_buddy)
{
buddy = _buddy;
}
void setClickable(bool _clickable);
void setBuddy(QWidget *_buddy);
QString getText() const
{
return bannerLabel->text();
}
void setClickable(bool _clickable)
{
clickable = _clickable;
}
protected:
void paintEvent(QPaintEvent *event) override;
private:
QVBoxLayout *layout;
QLabel *iconLabel;
QLabel *bannerLabel;
Qt::Orientation gradientOrientation;
int transparency; // Transparency percentage for the gradient
QWidget *buddy;
QWidget *buddy = nullptr;
bool clickable = true;
signals:
void buddyVisibilityChanged();
private slots:
void toggleBuddyVisibility() const;
void updateDropdownIconState() const;
};
#endif // BANNER_WIDGET_H

View File

@@ -7,8 +7,8 @@
#include <QWidget>
#include <qscrollarea.h>
inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget");
inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size");
inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget", QtInfoMsg);
inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size", QtInfoMsg);
class FlowWidget final : public QWidget
{

View File

@@ -19,7 +19,7 @@
* @param setInfoForCard The set information for the card.
*/
AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent,
TabDeckEditor *deckEditor,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,

View File

@@ -12,7 +12,7 @@ class AllZonesCardAmountWidget : public QWidget
Q_OBJECT
public:
explicit AllZonesCardAmountWidget(QWidget *parent,
TabDeckEditor *deckEditor,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,
@@ -31,7 +31,7 @@ public slots:
private:
QVBoxLayout *layout;
TabDeckEditor *deckEditor;
AbstractTabDeckEditor *deckEditor;
DeckListModel *deckModel;
QTreeView *deckView;
QSlider *cardSizeSlider;

View File

@@ -16,7 +16,7 @@
* @param zoneName The zone name (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE).
*/
CardAmountWidget::CardAmountWidget(QWidget *parent,
TabDeckEditor *deckEditor,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,

View File

@@ -4,7 +4,7 @@
#include "../../../../deck/deck_list_model.h"
#include "../../../../deck/deck_loader.h"
#include "../../../../game/cards/card_database.h"
#include "../../../tabs/tab_deck_editor.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "../general/display/dynamic_font_size_push_button.h"
#include <QHBoxLayout>
@@ -18,7 +18,7 @@ class CardAmountWidget : public QWidget
Q_OBJECT
public:
explicit CardAmountWidget(QWidget *parent,
TabDeckEditor *deckEditor,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,
@@ -36,7 +36,7 @@ protected:
void showEvent(QShowEvent *event) override;
private:
TabDeckEditor *deckEditor;
AbstractTabDeckEditor *deckEditor;
DeckListModel *deckModel;
QTreeView *deckView;
QSlider *cardSizeSlider;

View File

@@ -1,13 +1,15 @@
#include "printing_selector.h"
#include "../../../../settings/cache_settings.h"
#include "../../picture_loader/picture_loader.h"
#include "printing_selector_card_display_widget.h"
#include "printing_selector_card_search_widget.h"
#include "printing_selector_card_selection_widget.h"
#include "printing_selector_card_sorting_widget.h"
#include "printing_selector_view_options_toolbar_widget.h"
#include <QFrame>
#include <QScrollBar>
#include <qboxlayout.h>
/**
* @brief Constructs a PrintingSelector widget to display and manage card printings.
@@ -22,36 +24,72 @@
* @param deckModel The DeckListModel instance that provides data for the deck's contents.
* @param deckView The QTreeView instance used to display the deck and its contents.
*/
PrintingSelector::PrintingSelector(QWidget *parent,
TabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView)
: QWidget(parent), deckEditor(deckEditor), deckModel(deckModel), deckView(deckView)
PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deckEditor)
: QWidget(parent), deckEditor(_deckEditor), deckModel(deckEditor->deckDockWidget->deckModel),
deckView(deckEditor->deckDockWidget->deckView)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout = new QVBoxLayout();
layout = new QVBoxLayout(this);
setLayout(layout);
if (PictureLoader::hasCustomArt()) {
QFrame *warningFrame = new QFrame(this);
warningFrame->setFrameShape(QFrame::StyledPanel);
warningLabel = new QLabel(this);
warningLabel->setTextFormat(Qt::RichText);
warningLabel->setWordWrap(true);
auto *warningLayout = new QVBoxLayout(warningFrame);
warningFrame->setLayout(warningLayout);
warningLayout->addWidget(warningLabel);
layout->addWidget(warningFrame);
} else {
warningLabel = nullptr;
}
widgetLoadingBufferTimer = new QTimer(this);
// Initialize toolbar and widgets
viewOptionsToolbar = new PrintingSelectorViewOptionsToolbarWidget(this, this);
layout->addWidget(viewOptionsToolbar);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
sortToolBar = new PrintingSelectorCardSortingWidget(this);
sortToolBar->setVisible(SettingsCache::instance().getPrintingSelectorSortOptionsVisible());
layout->addWidget(sortToolBar);
sortToolBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
displayOptionsWidget = new SettingsButtonWidget(this);
displayOptionsWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
// Create the checkbox for navigation buttons visibility
navigationCheckBox = new QCheckBox(this);
navigationCheckBox->setChecked(SettingsCache::instance().getPrintingSelectorNavigationButtonsVisible());
connect(navigationCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
&PrintingSelector::toggleVisibilityNavigationButtons);
connect(navigationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setPrintingSelectorNavigationButtonsVisible);
cardSizeWidget =
new CardSizeWidget(displayOptionsWidget, flowWidget, SettingsCache::instance().getPrintingSelectorCardSize());
displayOptionsWidget->addSettingsWidget(sortToolBar);
displayOptionsWidget->addSettingsWidget(navigationCheckBox);
displayOptionsWidget->addSettingsWidget(cardSizeWidget);
sortAndOptionsContainer = new QWidget(this);
sortAndOptionsLayout = new QHBoxLayout(sortAndOptionsContainer);
sortAndOptionsLayout->setSpacing(3);
sortAndOptionsLayout->setContentsMargins(0, 0, 0, 0);
sortAndOptionsContainer->setLayout(sortAndOptionsLayout);
searchBar = new PrintingSelectorCardSearchWidget(this);
searchBar->setVisible(SettingsCache::instance().getPrintingSelectorSearchBarVisible());
layout->addWidget(searchBar);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
sortAndOptionsLayout->addWidget(searchBar);
sortAndOptionsLayout->addWidget(displayOptionsWidget);
layout->addWidget(sortAndOptionsContainer);
layout->addWidget(flowWidget);
cardSizeWidget = new CardSizeWidget(this, flowWidget, SettingsCache::instance().getPrintingSelectorCardSize());
cardSizeWidget->setVisible(SettingsCache::instance().getPrintingSelectorCardSizeSliderVisible());
layout->addWidget(cardSizeWidget);
cardSelectionBar = new PrintingSelectorCardSelectionWidget(this);
cardSelectionBar->setVisible(SettingsCache::instance().getPrintingSelectorNavigationButtonsVisible());
layout->addWidget(cardSelectionBar);
@@ -59,6 +97,19 @@ PrintingSelector::PrintingSelector(QWidget *parent,
// Connect deck model data change signal to update display
connect(deckModel, &DeckListModel::rowsInserted, this, &PrintingSelector::printingsInDeckChanged);
connect(deckModel, &DeckListModel::rowsRemoved, this, &PrintingSelector::printingsInDeckChanged);
retranslateUi();
}
void PrintingSelector::retranslateUi()
{
navigationCheckBox->setText(tr("Display Navigation Buttons"));
if (warningLabel) {
warningLabel->setText(
tr("<b>Warning:</b> You appear to be using custom card art, which has known bugs when also "
"using the printing selector in this version of Cockatrice."));
}
}
void PrintingSelector::printingsInDeckChanged()
@@ -206,36 +257,6 @@ void PrintingSelector::getAllSetsForCurrentCard()
widgetLoadingBufferTimer->start(0); // Process as soon as possible
}
/**
* @brief Toggles the visibility of the sorting options toolbar.
*
* @param _state The visibility state to set.
*/
void PrintingSelector::toggleVisibilitySortOptions(bool _state)
{
sortToolBar->setVisible(_state);
}
/**
* @brief Toggles the visibility of the search bar.
*
* @param _state The visibility state to set.
*/
void PrintingSelector::toggleVisibilitySearchBar(bool _state)
{
searchBar->setVisible(_state);
}
/**
* @brief Toggles the visibility of the card size slider.
*
* @param _state The visibility state to set.
*/
void PrintingSelector::toggleVisibilityCardSizeSlider(bool _state)
{
cardSizeWidget->setVisible(_state);
}
/**
* @brief Toggles the visibility of the navigation buttons.
*

View File

@@ -5,7 +5,9 @@
#include "../../../../game/cards/card_database.h"
#include "../cards/card_size_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include "../quick_settings/settings_button_widget.h"
#include <QCheckBox>
#include <QLabel>
#include <QTreeView>
#include <QVBoxLayout>
@@ -16,24 +18,23 @@
class PrintingSelectorCardSearchWidget;
class PrintingSelectorCardSelectionWidget;
class PrintingSelectorCardSortingWidget;
class PrintingSelectorViewOptionsToolbarWidget;
class TabDeckEditor;
class PrintingSelectorViewOptionsWidget;
class AbstractTabDeckEditor;
class PrintingSelector : public QWidget
{
Q_OBJECT
public:
PrintingSelector(QWidget *parent, TabDeckEditor *deckEditor, DeckListModel *deckModel, QTreeView *deckView);
PrintingSelector(QWidget *parent, AbstractTabDeckEditor *deckEditor);
void setCard(const CardInfoPtr &newCard, const QString &_currentZone);
void getAllSetsForCurrentCard();
public slots:
void retranslateUi();
void updateDisplay();
void selectPreviousCard();
void selectNextCard();
void toggleVisibilitySortOptions(bool _state);
void toggleVisibilitySearchBar(bool _state);
void toggleVisibilityCardSizeSlider(bool _state);
void toggleVisibilityNavigationButtons(bool _state);
private slots:
@@ -41,13 +42,17 @@ private slots:
private:
QVBoxLayout *layout;
PrintingSelectorViewOptionsToolbarWidget *viewOptionsToolbar;
SettingsButtonWidget *displayOptionsWidget;
QWidget *sortAndOptionsContainer;
QHBoxLayout *sortAndOptionsLayout;
QCheckBox *navigationCheckBox;
QLabel *warningLabel;
PrintingSelectorCardSortingWidget *sortToolBar;
PrintingSelectorCardSearchWidget *searchBar;
FlowWidget *flowWidget;
CardSizeWidget *cardSizeWidget;
PrintingSelectorCardSelectionWidget *cardSelectionBar;
TabDeckEditor *deckEditor;
AbstractTabDeckEditor *deckEditor;
DeckListModel *deckModel;
QTreeView *deckView;
CardInfoPtr selectedCard;

View File

@@ -19,16 +19,16 @@
* display.
*
* @param parent The parent widget for this display.
* @param deckEditor The TabDeckEditor instance for deck management.
* @param deckModel The DeckListModel instance providing deck data.
* @param deckView The QTreeView instance displaying the deck.
* @param cardSizeSlider The slider controlling the size of the displayed card.
* @param rootCard The root card object, representing the card to be displayed.
* @param setInfoForCard The set-specific information for the card being displayed.
* @param currentZone The current zone in which the card is located.
* @param _deckEditor The TabDeckEditor instance for deck management.
* @param _deckModel The DeckListModel instance providing deck data.
* @param _deckView The QTreeView instance displaying the deck.
* @param _cardSizeSlider The slider controlling the size of the displayed card.
* @param _rootCard The root card object, representing the card to be displayed.
* @param _setInfoForCard The set-specific information for the card being displayed.
* @param _currentZone The current zone in which the card is located.
*/
PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *parent,
TabDeckEditor *_deckEditor,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
QTreeView *_deckView,
QSlider *_cardSizeSlider,

View File

@@ -1,16 +1,13 @@
#ifndef PRINTING_SELECTOR_CARD_DISPLAY_WIDGET_H
#define PRINTING_SELECTOR_CARD_DISPLAY_WIDGET_H
#include "../../../../client/ui/widgets/cards/card_info_picture_widget.h"
#include "../../../../deck/deck_list_model.h"
#include "../../../../game/cards/card_database.h"
#include "../../../tabs/tab_deck_editor.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "printing_selector_card_overlay_widget.h"
#include "set_name_and_collectors_number_display_widget.h"
#include <QPainter>
#include <QTreeView>
#include <QVBoxLayout>
#include <QWidget>
class PrintingSelectorCardDisplayWidget : public QWidget
@@ -19,7 +16,7 @@ class PrintingSelectorCardDisplayWidget : public QWidget
public:
PrintingSelectorCardDisplayWidget(QWidget *parent,
TabDeckEditor *_deckEditor,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
QTreeView *_deckView,
QSlider *_cardSizeSlider,
@@ -36,7 +33,7 @@ signals:
private:
QVBoxLayout *layout;
SetNameAndCollectorsNumberDisplayWidget *setNameAndCollectorsNumberDisplayWidget;
TabDeckEditor *deckEditor;
AbstractTabDeckEditor *deckEditor;
DeckListModel *deckModel;
QTreeView *deckView;
QSlider *cardSizeSlider;

View File

@@ -17,15 +17,15 @@
* as a widget that displays the card amounts in different zones (mainboard, sideboard, etc.).
*
* @param parent The parent widget for this overlay.
* @param deckEditor The TabDeckEditor instance for deck management.
* @param deckModel The DeckListModel instance providing deck data.
* @param deckView The QTreeView instance displaying the deck.
* @param cardSizeSlider The slider controlling the size of the card.
* @param rootCard The root card object that contains information about the card.
* @param setInfoForCard The set-specific information for the card being displayed.
* @param _deckEditor The TabDeckEditor instance for deck management.
* @param _deckModel The DeckListModel instance providing deck data.
* @param _deckView The QTreeView instance displaying the deck.
* @param _cardSizeSlider The slider controlling the size of the card.
* @param _rootCard The root card object that contains information about the card.
* @param _setInfoForCard The set-specific information for the card being displayed.
*/
PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *parent,
TabDeckEditor *_deckEditor,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
QTreeView *_deckView,
QSlider *_cardSizeSlider,
@@ -120,7 +120,7 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEvent *event)
#endif
{
QWidget::enterEvent(event);
deckEditor->updateCardInfo(setCard);
deckEditor->updateCard(setCard);
// Check if either mainboard or sideboard amount is greater than 0
if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) {
@@ -199,7 +199,7 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(relatedCard, &QAction::triggered, deckEditor, [this, relatedCardName] {
deckEditor->updateCardInfo(CardDatabaseManager::getInstance()->getCard(relatedCardName));
deckEditor->updateCard(CardDatabaseManager::getInstance()->getCard(relatedCardName));
deckEditor->showPrintingSelector();
});
}

View File

@@ -4,7 +4,7 @@
#include "../../../../client/ui/widgets/cards/card_info_picture_widget.h"
#include "../../../../deck/deck_list_model.h"
#include "../../../../game/cards/card_database.h"
#include "../../../tabs/tab_deck_editor.h"
#include "../../../tabs/abstract_tab_deck_editor.h"
#include "all_zones_card_amount_widget.h"
#include "card_amount_widget.h"
#include "set_name_and_collectors_number_display_widget.h"
@@ -15,7 +15,7 @@ class PrintingSelectorCardOverlayWidget : public QWidget
public:
explicit PrintingSelectorCardOverlayWidget(QWidget *parent,
TabDeckEditor *_deckEditor,
AbstractTabDeckEditor *_deckEditor,
DeckListModel *_deckModel,
QTreeView *_deckView,
QSlider *_cardSizeSlider,
@@ -39,7 +39,7 @@ signals:
private:
CardInfoPictureWidget *cardInfoPicture;
AllZonesCardAmountWidget *allZonesCardAmountWidget;
TabDeckEditor *deckEditor;
AbstractTabDeckEditor *deckEditor;
DeckListModel *deckModel;
QTreeView *deckView;
QSlider *cardSizeSlider;

View File

@@ -18,10 +18,11 @@ const QStringList PrintingSelectorCardSortingWidget::SORT_OPTIONS = {SORT_OPTION
*/
PrintingSelectorCardSortingWidget::PrintingSelectorCardSortingWidget(PrintingSelector *parent) : parent(parent)
{
setMinimumWidth(300);
sortToolBar = new QHBoxLayout(this);
sortToolBar->setContentsMargins(9, 0, 9, 0);
sortOptionsSelector = new QComboBox(this);
sortOptionsSelector->setFocusPolicy(Qt::StrongFocus);
sortOptionsSelector->addItems(SORT_OPTIONS);
sortOptionsSelector->setCurrentIndex(SettingsCache::instance().getPrintingSelectorSortOrder());
connect(sortOptionsSelector, &QComboBox::currentTextChanged, this,

View File

@@ -1,140 +0,0 @@
#include "printing_selector_view_options_toolbar_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
/**
* @class PrintingSelectorViewOptionsToolbarWidget
* @brief A widget that provides a toolbar for view options with collapsible and expandable functionality.
*
* This widget allows the user to collapse or expand the view options for the PrintingSelector,
* providing a more compact interface when collapsed and a full view of options when expanded.
*/
PrintingSelectorViewOptionsToolbarWidget::PrintingSelectorViewOptionsToolbarWidget(QWidget *_parent,
PrintingSelector *_printingSelector)
: QWidget(_parent), printingSelector(_printingSelector)
{
// Set up layout for the widget
layout = new QVBoxLayout();
layout->setContentsMargins(9, 0, 9, 0);
layout->setSpacing(0);
setLayout(layout);
// Set up the expanded widget with its layout
expandedWidget = new QWidget(this);
auto *expandedLayout = new QVBoxLayout(expandedWidget);
expandedLayout->setContentsMargins(0, 0, 0, 0);
expandedLayout->setSpacing(0);
// Collapse button to toggle between expanded and collapsed states
collapseButton = new QPushButton("", this);
collapseButton->setFixedSize(20, 20);
collapseButton->setToolTip("Collapse");
collapseButton->setStyleSheet("border: none;");
connect(collapseButton, &QPushButton::clicked, this, &PrintingSelectorViewOptionsToolbarWidget::collapse);
expandedLayout->addWidget(collapseButton, 0, Qt::AlignLeft);
// View options widget
viewOptions = new PrintingSelectorViewOptionsWidget(expandedWidget, printingSelector);
expandedLayout->addWidget(viewOptions);
expandedWidget->setLayout(expandedLayout);
// Set up the collapsed widget with its layout
collapsedWidget = new QWidget(this);
auto *collapsedLayout = new QHBoxLayout(collapsedWidget);
collapsedLayout->setContentsMargins(5, 0, 5, 0);
collapsedLayout->setSpacing(0);
// Expand button to show full options
expandButton = new QPushButton("", this);
expandButton->setFixedSize(20, 20);
expandButton->setToolTip("Expand");
expandButton->setStyleSheet("border: none;");
connect(expandButton, &QPushButton::clicked, this, &PrintingSelectorViewOptionsToolbarWidget::expand);
collapsedLayout->addWidget(expandButton);
// Label for collapsed state
auto *collapsedLabel = new QLabel(tr("Display Options"), this);
collapsedLayout->addWidget(collapsedLabel);
collapsedWidget->setLayout(collapsedLayout);
// Stack widget to switch between expanded and collapsed states
stackedWidget = new QStackedWidget(this);
stackedWidget->addWidget(expandedWidget);
stackedWidget->addWidget(collapsedWidget);
layout->addWidget(stackedWidget);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
// Default to the expanded widget
stackedWidget->setCurrentWidget(expandedWidget);
// Connect the stacked widget to update the layout when it changes
connect(stackedWidget, &QStackedWidget::currentChanged, this,
&PrintingSelectorViewOptionsToolbarWidget::onWidgetChanged);
}
/**
* @brief Toggles the widget to the collapsed state.
*/
void PrintingSelectorViewOptionsToolbarWidget::collapse()
{
stackedWidget->setCurrentWidget(collapsedWidget);
updateGeometry();
}
/**
* @brief Toggles the widget to the expanded state.
*/
void PrintingSelectorViewOptionsToolbarWidget::expand()
{
stackedWidget->setCurrentWidget(expandedWidget);
updateGeometry();
}
/**
* @brief Handles the geometry update when the stacked widget changes.
*
* This ensures that the parent layout is also updated when the widget's display state changes.
*/
void PrintingSelectorViewOptionsToolbarWidget::onWidgetChanged(int)
{
updateGeometry();
if (parentWidget() && parentWidget()->layout()) {
parentWidget()->layout()->invalidate();
}
}
/**
* @brief Provides the recommended size for the widget based on the current view.
*
* @return QSize The suggested size for the widget.
*/
QSize PrintingSelectorViewOptionsToolbarWidget::sizeHint() const
{
return stackedWidget->currentWidget()->sizeHint();
}
/**
* @brief Provides the minimum size required for the widget based on the current view.
*
* @return QSize The minimum size required for the widget.
*/
QSize PrintingSelectorViewOptionsToolbarWidget::minimumSizeHint() const
{
return stackedWidget->currentWidget()->minimumSizeHint();
}
/**
* @brief Returns the view options widget contained within this toolbar.
*
* @return PrintingSelectorViewOptionsWidget* The view options widget.
*/
PrintingSelectorViewOptionsWidget *PrintingSelectorViewOptionsToolbarWidget::getViewOptionsWidget() const
{
return viewOptions;
}

View File

@@ -1,36 +0,0 @@
#ifndef PRINTING_SELECTOR_SORT_AND_SEARCH_TOOLBAR_WIDGET_H
#define PRINTING_SELECTOR_SORT_AND_SEARCH_TOOLBAR_WIDGET_H
#include "printing_selector.h"
#include "printing_selector_view_options_widget.h"
#include <QPushButton>
#include <QStackedWidget>
#include <QVBoxLayout>
#include <QWidget>
class PrintingSelectorViewOptionsToolbarWidget : public QWidget
{
Q_OBJECT
public:
explicit PrintingSelectorViewOptionsToolbarWidget(QWidget *parent, PrintingSelector *printingSelector);
void collapse();
void expand();
void onWidgetChanged(int);
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
PrintingSelectorViewOptionsWidget *getViewOptionsWidget() const;
private:
QVBoxLayout *layout;
PrintingSelector *printingSelector;
PrintingSelectorViewOptionsWidget *viewOptions;
QWidget *expandedWidget;
QPushButton *collapseButton;
QWidget *collapsedWidget;
QPushButton *expandButton;
QStackedWidget *stackedWidget;
};
#endif // PRINTING_SELECTOR_SORT_AND_SEARCH_TOOLBAR_WIDGET_H

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