From 3bf9de18b12105d136d113f386c867984c0473c6 Mon Sep 17 00:00:00 2001 From: GenevensiS <66968533+G-e-n-e-v-e-n-s-i-S@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:17:13 +0200 Subject: [PATCH] Implement unique IDs and card linking --- CMakeLists.txt | 5 +- CMakeSettings.json | 2 +- doc/about/license.txt | 120 ++--- doc/file/format.txt | 14 +- doc/function/crop.txt | 10 +- doc/function/dimensions_of.txt | 11 + doc/function/get_card_from_link.txt | 13 + doc/function/get_card_from_uid.txt | 12 + doc/function/get_card_styling.txt | 4 +- doc/function/get_mse_locale.txt | 6 + doc/function/has_link.txt | 14 + doc/function/index.txt | 6 + doc/function/insert_image.txt | 17 + doc/script/predefined_variables.txt | 4 +- doc/type/field.txt | 10 +- resource/tool/card_copy.png | Bin 0 -> 4405 bytes resource/tool/card_link.png | Bin 0 -> 4456 bytes resource/win32_res.rc | 2 + src/data/action/set.cpp | 85 +++- src/data/action/set.hpp | 31 ++ src/data/card.cpp | 232 ++++++++- src/data/card.hpp | 27 ++ src/data/field.cpp | 42 +- src/data/field.hpp | 2 +- src/data/font.cpp | 100 ++-- src/data/font.hpp | 19 +- src/data/format/clipboard.cpp | 3 + src/data/format/formats.hpp | 5 +- src/data/format/image.cpp | 122 +++-- src/data/game.cpp | 41 +- src/data/game.hpp | 6 +- src/data/locale.cpp | 8 +- src/data/settings.cpp | 10 +- src/data/settings.hpp | 8 +- src/data/stylesheet.cpp | 17 +- src/data/stylesheet.hpp | 12 +- src/data/symbol_font.cpp | 9 +- src/data/symbol_font.hpp | 5 +- src/gfx/blend_image.cpp | 6 +- src/gfx/combine_image.cpp | 10 +- src/gfx/generated_image.cpp | 266 +++++----- src/gfx/generated_image.hpp | 54 +-- src/gfx/gfx.hpp | 2 +- src/gui/card_link_window.cpp | 111 +++++ src/gui/card_link_window.hpp | 52 ++ src/gui/control/card_editor.cpp | 23 + src/gui/control/card_editor.hpp | 1 + src/gui/control/card_list.cpp | 60 ++- src/gui/control/card_list.hpp | 17 +- src/gui/control/card_viewer.cpp | 25 +- src/gui/control/card_viewer.hpp | 6 +- src/gui/control/item_list.cpp | 10 +- src/gui/control/item_list.hpp | 4 +- src/gui/drop_down_list.hpp | 18 +- src/gui/image_slice_window.cpp | 237 +++++---- src/gui/image_slice_window.hpp | 47 +- src/gui/new_window.cpp | 14 +- src/gui/new_window.hpp | 6 +- src/gui/preferences_window.cpp | 16 +- src/gui/print_window.cpp | 456 +++++++++--------- src/gui/set/cards_panel.cpp | 322 ++++++++++--- src/gui/set/cards_panel.hpp | 13 +- src/gui/set/console_panel.cpp | 90 ++-- src/gui/set/console_panel.hpp | 6 +- src/gui/set/set_info_panel.cpp | 6 +- src/gui/set/style_panel.cpp | 28 +- src/gui/symbol/part_list.cpp | 4 +- src/gui/value/image.cpp | 35 +- src/gui/value/image.hpp | 4 +- src/gui/value/text.cpp | 18 +- src/main.cpp | 10 +- src/render/text/element.cpp | 4 +- src/render/value/choice.cpp | 7 +- src/render/value/image.cpp | 2 +- src/render/value/information.cpp | 22 +- src/script/context.cpp | 10 +- src/script/context.hpp | 4 +- src/script/functions/basic.cpp | 98 +++- src/script/functions/construction.cpp | 162 +++---- src/script/functions/english.cpp | 2 +- src/script/functions/export.cpp | 12 +- src/script/functions/image.cpp | 99 ++-- src/script/parser.cpp | 2 +- src/script/script.cpp | 4 +- src/script/script.hpp | 4 +- src/script/script_manager.cpp | 22 +- src/util/io/package.cpp | 4 +- src/util/io/package_manager.cpp | 2 +- src/util/io/reader.cpp | 8 +- src/util/io/reader.hpp | 8 +- src/util/locale.hpp | 12 +- src/util/string.cpp | 4 +- src/util/string.hpp | 6 +- src/util/tagged_string.cpp | 12 +- src/util/uid.hpp | 31 ++ src/util/version.cpp | 16 +- src/util/window_id.hpp | 33 +- tools/website/drupal/autoformat.module | 34 +- .../drupal/mse-drupal-modules/autoformat.inc | 2 +- .../drupal/mse-drupal-modules/highlight.inc | 14 +- 100 files changed, 2432 insertions(+), 1219 deletions(-) create mode 100644 doc/function/dimensions_of.txt create mode 100644 doc/function/get_card_from_link.txt create mode 100644 doc/function/get_card_from_uid.txt create mode 100644 doc/function/get_mse_locale.txt create mode 100644 doc/function/has_link.txt create mode 100644 doc/function/insert_image.txt create mode 100644 resource/tool/card_copy.png create mode 100644 resource/tool/card_link.png create mode 100644 src/gui/card_link_window.cpp create mode 100644 src/gui/card_link_window.hpp create mode 100644 src/util/uid.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e74425a3..741d0404 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.13) -project(magicseteditor VERSION 2.5.8) +project(magicseteditor VERSION 2.6.0) + add_definitions(-DUNOFFICIAL_BUILD) set(CMAKE_CXX_STANDARD 17) @@ -28,7 +29,7 @@ endif() # You will most likely get a message about being unable to open hunspell-1.7.lib because pkgconf forgets to add the actual path to # HUNSPELL_LIBRARIES. If so, uncomment the below line and point it to the correct vcpkg root folder/library. -#set(HUNSPELL_LIBRARIES "C:\\PATH\\TO\\ROOT\\vcpkg\\installed\\${VCPKG_TARGET_TRIPLET}\\lib\\hunspell-1.7.lib") +set(HUNSPELL_LIBRARIES "C:\\src\\vcpkg\\installed\\${VCPKG_TARGET_TRIPLET}\\lib\\hunspell-1.7.lib") message("-- Does this have a full path? If not, and it's just a file name, it's broken: Found Hunspell at ${HUNSPELL_LIBRARIES}") include_directories("${PROJECT_BINARY_DIR}/src") diff --git a/CMakeSettings.json b/CMakeSettings.json index b91aaa91..c4ae2e2d 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -74,4 +74,4 @@ "inheritEnvironments": [ "msvc_x86" ] } ] -} +} diff --git a/doc/about/license.txt b/doc/about/license.txt index 3557979b..abc94fc4 100644 --- a/doc/about/license.txt +++ b/doc/about/license.txt @@ -322,63 +322,63 @@ POSSIBILITY OF SUCH DAMAGES. ==END OF TERMS AND CONDITIONS== - -==How to Apply These Terms to Your New Programs== - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - -] -] Copyright (C) -] -] This program is free software; you can redistribute it and/or modify -] it under the terms of the GNU General Public License as published by -] the Free Software Foundation; either version 2 of the License, or -] (at your option) any later version. -] -] This program is distributed in the hope that it will be useful, -] but WITHOUT ANY WARRANTY; without even the implied warranty of -] MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -] GNU General Public License for more details. -] -] You should have received a copy of the GNU General Public License -] along with this program; if not, write to the Free Software -] Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - -] Gnomovision version 69, Copyright (C) year name of author -] Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. -] This is free software, and you are welcome to redistribute it -] under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - -] Yoyodyne, Inc., hereby disclaims all copyright interest in the program -] `Gnomovision' (which makes passes at compilers) written by James Hacker. -] -] , 1 April 1989 -] Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. + +==How to Apply These Terms to Your New Programs== + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + +] +] Copyright (C) +] +] This program is free software; you can redistribute it and/or modify +] it under the terms of the GNU General Public License as published by +] the Free Software Foundation; either version 2 of the License, or +] (at your option) any later version. +] +] This program is distributed in the hope that it will be useful, +] but WITHOUT ANY WARRANTY; without even the implied warranty of +] MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +] GNU General Public License for more details. +] +] You should have received a copy of the GNU General Public License +] along with this program; if not, write to the Free Software +] Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + +] Gnomovision version 69, Copyright (C) year name of author +] Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +] This is free software, and you are welcome to redistribute it +] under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + +] Yoyodyne, Inc., hereby disclaims all copyright interest in the program +] `Gnomovision' (which makes passes at compilers) written by James Hacker. +] +] , 1 April 1989 +] Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/doc/file/format.txt b/doc/file/format.txt index d4ab464b..87ec02bf 100644 --- a/doc/file/format.txt +++ b/doc/file/format.txt @@ -33,13 +33,13 @@ A heirachical file can contain a reference to another file: Where filename must be an absolute or relative [[type:filename]]. That file is included literally into the current one; except for indentation, the included file never escapes from the level the 'include file' line is on. - -If the file to be included can vary depending on the locale that is selected, use: ->>>include localized file: filename - -MSE will take the filename and add "_" followed by the name of the currently selected locale at the end of it. -So for example, if the locale used is the folder "en.mse-locale", the file that will be included is "filename_en" -You must provide a version of the file for each locale found in the data folder, even if it is simply a copy of the english one. + +If the file to be included can vary depending on the locale that is selected, use: +>>>include localized file: filename + +MSE will take the filename and add "_" followed by the name of the currently selected locale at the end of it. +So for example, if the locale used is the folder "en.mse-locale", the file that will be included is "filename_en" +You must provide a version of the file for each locale found in the data folder, even if it is simply a copy of the english one. --Example-- For example, a [[type:set]] might look like this: diff --git a/doc/function/crop.txt b/doc/function/crop.txt index cff79709..c537cb0a 100644 --- a/doc/function/crop.txt +++ b/doc/function/crop.txt @@ -5,11 +5,13 @@ Function: crop Shrink an image by cutting off some of the image, starting at the position denoted by the offsets. The resulting image size is specified in the parameters. +Resulting image can be bigger than the original, if offset_x or offset_y are negative, or if width or height are bigger than the original width and height. + --Parameters-- ! Parameter Type Description | @input@ [[type:image]] Image to enlarge -| @height@ [[type:double]] Height of the resulting image -| @width@ [[type:double]] Width of the resulting image -| @offset_x@ [[type:double]] Offset of crop, horizontally -| @offset_y@ [[type:double]] Offset of crop, vertically +| @height@ [[type:double]] Height of the resulting image, in pixels +| @width@ [[type:double]] Width of the resulting image, in pixels +| @offset_x@ [[type:double]] Offset of crop, horizontally, in pixels +| @offset_y@ [[type:double]] Offset of crop, vertically, in pixels diff --git a/doc/function/dimensions_of.txt b/doc/function/dimensions_of.txt new file mode 100644 index 00000000..67f8e996 --- /dev/null +++ b/doc/function/dimensions_of.txt @@ -0,0 +1,11 @@ +Function: dimensions_of + +--Usage-- +> dimensions_of(input: image) + +Returns an array containing the width and height of the image in pixels. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:image]] Image to whos dimensions we want. + diff --git a/doc/function/get_card_from_link.txt b/doc/function/get_card_from_link.txt new file mode 100644 index 00000000..c84ab731 --- /dev/null +++ b/doc/function/get_card_from_link.txt @@ -0,0 +1,13 @@ +Function: get_card_from_link + +--Usage-- +> get_card_from_link(card: card, "link type") + +Inspects a given [[type:card]]'s links to find one of the given type, and returns the linked card. +Returns nil if no card was found. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:string]] The type of link we want to find. +| @card@ [[type:card]] The card whose links we'll inspect. +| @set@ [[type:set]] The set in which to look. This can be omited since 'set' is a predefined variable. diff --git a/doc/function/get_card_from_uid.txt b/doc/function/get_card_from_uid.txt new file mode 100644 index 00000000..b8e5a54e --- /dev/null +++ b/doc/function/get_card_from_uid.txt @@ -0,0 +1,12 @@ +Function: get_card_from_uid + +--Usage-- +> get_card_from_uid(input: "uid") + +Returns the [[type:card]] with the given uid inside the set. +Returns nil if no card was found. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:string]] The uid of the card we want to retrieve. +| @set@ [[type:set]] The set in which to look. This can be omited since 'set' is a predefined variable. diff --git a/doc/function/get_card_styling.txt b/doc/function/get_card_styling.txt index 34a1c17a..2ae7cb42 100644 --- a/doc/function/get_card_styling.txt +++ b/doc/function/get_card_styling.txt @@ -5,12 +5,12 @@ Function: get_card_styling Get the styling data of a [[type:card]]. -This is for use in exporter scripts. In card scripts, use the "styling" predefined variable instead. +This is for use in exporter scripts. In card scripts, use the 'styling' predefined variable instead. --Parameters-- ! Parameter Type Description | @input@ [[type:card]] The card you want to retrieve the styling data from. -| @set@ [[type:set]] The set the card belongs to. In an exporter script, this can be omited since "set" is a predefined variable. +| @set@ [[type:set]] The set the card belongs to. In an exporter script, this can be omited since 'set' is a predefined variable. --Examples-- > # Retrieve the value "is foil" from the card's styling options diff --git a/doc/function/get_mse_locale.txt b/doc/function/get_mse_locale.txt new file mode 100644 index 00000000..881fdb26 --- /dev/null +++ b/doc/function/get_mse_locale.txt @@ -0,0 +1,6 @@ +Function: get_mse_locale + +--Usage-- +> get_mse_locale() + +Returns the name of the currently selected locale folder. diff --git a/doc/function/has_link.txt b/doc/function/has_link.txt new file mode 100644 index 00000000..99af105c --- /dev/null +++ b/doc/function/has_link.txt @@ -0,0 +1,14 @@ +Function: has_link + +--Usage-- +> has_link(card: card, "link type") + +Inspects a given [[type:card]]'s links to find one of the given type. +Returns true if such a link was found, false otherwise. + +Note that this function does not check if the linked card exists in the set. For that, use get_card_from_link. + +--Parameters-- +! Parameter Type Description +| @input@ [[type:string]] The type of link we want to find. +| @card@ [[type:card]] The card whose links we'll inspect. diff --git a/doc/function/index.txt b/doc/function/index.txt index 731336a8..b363f26a 100644 --- a/doc/function/index.txt +++ b/doc/function/index.txt @@ -97,6 +97,8 @@ These functions are built into the program, other [[type:function]]s can be defi | [[fun:flip_vertical]] Flip an image vertically. | [[fun:rotate_image]] Rotate an image. | [[fun:drop_shadow]] Add a drop shadow to an image. +| [[fun:insert_image]] Insert an image inside another. +| [[fun:dimensions_of]] Get the width and height of an image. | [[fun:symbol_variation]] Render a variation of a [[type:symbol]]. | [[fun:import_image]] Load an image from outside the data folder. | [[fun:built_in_image]] Return an image built into the program. @@ -106,6 +108,9 @@ These functions are built into the program, other [[type:function]]s can be defi | [[fun:add_card_to_set]] Add a [[type:card]] to a [[type:set]]. | [[fun:get_card_styling]] Get the styling data of a [[type:card]]. | [[fun:get_card_stylesheet]] Get the stylesheet of a [[type:card]]. +| [[fun:get_card_from_uid]] Find the [[type:card]] with the given uid. +| [[fun:get_card_from_link]] Find a [[type:card]] that has the given link type to the given [[type:card]]. +| [[fun:has_link]] Determine if the given the given [[type:card]] has a link of the given type. ! HTML export <<< | [[fun:to_html]] Convert [[type:tagged text]] to html. @@ -118,6 +123,7 @@ These functions are built into the program, other [[type:function]]s can be defi ! Other functions <<< | [[fun:get_mse_version]] Get the MSE app version. +| [[fun:get_mse_locale]] Get the name of the currently selected locale. | [[fun:get_mse_path]] Get the MSE app folder absolute path. | [[fun:trace]] Output a message for debugging purposes. | [[fun:assert]] Check a condition for debugging purposes. diff --git a/doc/function/insert_image.txt b/doc/function/insert_image.txt new file mode 100644 index 00000000..c6792da2 --- /dev/null +++ b/doc/function/insert_image.txt @@ -0,0 +1,17 @@ +Function: insert_image + +--Usage-- +> insert_image(base_image: image, inserted_image: image, offset_x: coordinate, offset_y: coordinate, background_color: color) + +Insert an image inside another image. + +The inserted image can be put outside the bounds of the base image. The resulting image will be widened accordingly. + +--Parameters-- +! Parameter Type Description +| @base_image@ [[type:image]] Image that serves as the canvas +| @inserted_image@ [[type:image]] Image inserted on top of the base image +| @offset_x@ [[type:double]] Offset of insertion, horizontally, in pixels +| @offset_y@ [[type:double]] Offset of insertion, vertically, in pixels +| @background_color@ [[type:color]] Background color, optional, defaults to transparent + diff --git a/doc/script/predefined_variables.txt b/doc/script/predefined_variables.txt index 18aff28c..bd5e6294 100644 --- a/doc/script/predefined_variables.txt +++ b/doc/script/predefined_variables.txt @@ -9,8 +9,8 @@ Aside from the [[fun:index|built in functions]] the following variables are prov The current stylesheet | @card@ [[type:card]] not in @init script@s or when exporting The current card. -| @card_style@ [[type:indexmap]] of [[type:style]]s where @card@ is available Style properties for the current card, the same as @stylesheet.card_style@. -| @extra_card@ [[type:indexmap]] of [[type:value]]s field values for the current card as defined by the stylesheet. +| @card_style@ [[type:indexmap]] of [[type:style]]s where @card@ is available Style properties for the current card, the same as @stylesheet.card_style@. +| @extra_card@ [[type:indexmap]] of [[type:value]]s field values for the current card as defined by the stylesheet. | @extra_card_style@ [[type:indexmap]] of [[type:style]] where @card@ is available Style properties for the current card as added by the stylesheet. | @styling@ [[type:indexmap]] of [[type:value]]s where @card@ is available Styling options for the stylesheet/card. | @value@ [[type:value]] when evaluating a [[type:field]]'s @script@ or @default@ script Current value in the field. diff --git a/doc/type/field.txt b/doc/type/field.txt index a024e7d2..b6414df3 100644 --- a/doc/type/field.txt +++ b/doc/type/field.txt @@ -43,9 +43,9 @@ Fields are part of the [[file:style triangle]]: | @card list name@ [[type:localized string]] field name Alternate name to use for the card list, for example an abbreviation. | @card list alignment@ [[type:alignment]] @left@ Alignment of the card list column. | @sort script@ [[type:script]] Alternate way to sort the card list when using this column to sort the list. -| @import script@ [[type:script]] Script applied to the value given when creating a card with the new_card function. The script may return a map from field names to values. - For example, the pt field should not be initialized directly, since it is a combination of the power field and toughness field. - So if a value is given for pt, it must be redirected to power and toughness like so: {split := split_text(value, match:"/"); [power:split[0], toughness:split[1]]}. +| @import script@ [[type:script]] Script applied to the value given when creating a card with the new_card function. The script may return a map from field names to values. + For example, the pt field should not be initialized directly, since it is a combination of the power field and toughness field. + So if a value is given for pt, it must be redirected to power and toughness like so: {split := split_text(value, match:"/"); [power:split[0], toughness:split[1]]}. Use the make_map function to dynamically create maps. The @type@ determines what values of this field contain: @@ -85,8 +85,8 @@ Additional properties are available, depending on the type of field: These choices must appear in the same order as they do in the @choices@ property. | @"boolean"@ ''A boolean field is a choice field with the choices @"yes"@ and @"no"@.'' <<< <<< <<< - -| @"slider"@ ''A slider field is a choice field where the choices are all numbers.'' <<< <<< <<< + +| @"slider"@ ''A slider field is a choice field where the choices are all numbers.'' <<< <<< <<< | ^^^ @script@ [[type:script]] Script to apply to values of this field after each change.
If the script evaluates to a constant (i.e. doesn't use @value@) then values in this field can effectively not be edited. | ^^^ @default@ [[type:script]] Script to determine the value when it is in the default state (not edited). diff --git a/resource/tool/card_copy.png b/resource/tool/card_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..67ac5a48f44089b5aba30ab82dda7b9b7033d83b GIT binary patch literal 4405 zcmeHLeNYtV8DBsmUO=ev1Dk5rLnn~9x4ZYf8%|>HP^cU|;Z(3uv$wkkTeuHy7w(`a z1k@m=2zAm#V@#}?&Jc76=0l-I2vA~6utkku!$(krOP$g>t?dk2`tBYew3*2;ll;fc z?(M$M`#itrd4A9HzH{&Gv!t$%ko`~wK~O|;k|_;5qr^Kb1boLLk?G*kTx87<(kL(N z_PFd^E(;5VZWd;JoE?IEHyxH+5v^F*!>r0G=!qAws>>(Rbi@B{f5n=2>tOMk$p*@YeZ(UG8|wm-D~))L>oi{E{-VXX71)YL26uI{ht zJyYKmdhj3D(Bli19N4$FxBK(%iVX{6BZ`Wje9P~7^jYKW@Lz_Mn}$9p8EF6HlT9VB zZn$wM`t3tM3va${?f+|8`T3f&=N5lluKA*P#n=NOxcB<%%q1V4&#Q!>pxvC&Xh}92 zCvpK99NAf8NNRmCdT-x*KS_LU*}ZoMs*jPGQH%0cZ(3cpr+ahu)s{Qm3EAD%C(i0+ zr$gtKEPj8!qN|ZSD=X`dtZ3>#F)UxT>y3q5+k72)ks~`t{@CL!{ZpOt>b-qIwf3cc z(@UL$TiTnSf2cOT*;e0{VmOr9`59l_)7tT`rr1mOPml89v7rZp*3{^KFSN>g;iEkt z3SFaZ)aG9=9N-7$l`E1Sex840P}jBU)zG6$wGIBl0qlc}uWje{XWp>v%c_W}tlju& zHJ|y_@7gm@eVI9W^x>YCvX6iD6?tmo%KF_a)1p=!uaec)N1faj({4K4b$j`Le$erb z{Ed{;vS?zlrCfIL@8x4HBfW>){c*au=($BZa!2X#uzSI$_Bu+QxmEC&Ie$wC*|N1` zM@}&R&Iuj4^-frrBV@_7W#--xg}3zBv)hy_*WbUqpfa)l(QBW^MTYkVKgx0T-5k>I zgc`2AKlpTie^fNO@j+up(`e)91DCm}B%yZ1bN+Y@pn>p(PJ!|vPCiD22w<34 zp7wBVfpa-w5tFjH@&$uj4(8#B_#AGtc@o~qkFx;yKz)=O#T6LpaG+BycpfQ$$9 zjTXEWEKxL#g-Agb z=Wt6_fb40S0%w0r*0k8f87ZBqfdKPK+-ceqxl77G%WNi1E;?TfFWF>}i~dQ*MRN=( zz2a&euE(@0gkseMqPD3pgurM5QK~Uq$1*m8&|p)blAXLjIcZje0^kY`;MiC~hY@NT zVQ~yc)OLbJC`xHV^q5VB>v5V;=Iv1@E99fnI#3{9?cc^nidC+DEDSk&#zk|spL$+ebbgIuWy1TIM|xs+fB z24D?vPR8ZsA1hlq2b(TXBA>WU4N}k&dP0xsF-^dgWWSm9@Sqn(R2);Nw34}47!srd zsHMbC1pty9WJ4M~EG4)+R+lT+AQvHE(QFka36zNvSOAJCH6*42mlfBNYJybj zpT}?#o1*VxID6r@X^YDTjt>+)iQ|F)LP-=@Q|WA8U=$eUa?)agVQE>A6dg!`rwUj` ziW6W3rs!+n5#0MN5eX2kC-U9)t}h=Ca?&vw@=T{B`} zM#{6@^_|frdwiH;o!}Rc7aWzU_CEI>IA+n76l)@QHa9nymX?l2IqWYPs;mN0$co2rv2Ew^QvN>_CKO`g)$JLwiDIjtro7P&RFSAt}<~PG(p)IE0 z{If~<^O2HEWi?kmJg^8=&1u%vmlbvjCy9o|HD8-wk99s#ezK@&c>l@E#%ucx@y&OO zGnOBScqzK;z7O3lBUUXR>4{shJ-k=5>a}(A&RnPtPM8zMFMtwk-WMWw6oxLX4eAkN PfszwbO{ahSa@l_Y#tKGQ literal 0 HcmV?d00001 diff --git a/resource/tool/card_link.png b/resource/tool/card_link.png new file mode 100644 index 0000000000000000000000000000000000000000..087a44c564ac8cfc3cdd01d8979fec212b57bc37 GIT binary patch literal 4456 zcmeHKdr%X19uFV{qf%6!1*hrC)pD5ZX0r*&9#I1cl4^KYqi5C4?gkb}He?qP7(E|% z^r+Gi)LyBh^{&#>Y4I_0y+ZLp!Q$0xD+<=T;VMTdo)s0OJqv0J_uKHE>&y)^*Z)W+ z`}jUSzwh_+{eFLw-z({Dx?@AwIS6`r(mAtcPu(kB%mRNVzEo;qS zZbAOB!lApbFMO;g-}=KKl1CW6elm|Fp#N}tZYQBbkHVntUS3b~H|4e_MuCLQ4@mO=VGT1-+GcDsC(Oq#T@1Iuh{2;CE zu3ffm&c9i|@W0v3oqlrn&ZcEAp)sq!X%CV3cl}UtAyz#93*Ynk4))~s!z)CQy?KuU zG@3b=_8vUnH_-RymCj*ZdvCw^a+K?G#tzB_ny#1u&1EsK#YxsKCn(lR%RP1{=sJ-o zVWr1Okohz(wbD6^LkIPrZG@x@rGwV1%!t{UMCURor7k+FG&P$n%_p@Kv~qLW5QcRA?5t-4$6r<1zi8R4Q+3-w5 z@)!ho(?PjB@5Eu)?RLxEN;&JwffZV<7DiAQMP0- ztV1eb5>~c|*Fg}_OUL80JI&?^cn3Ge0^kGo5KdSjM_{`hp6bEz#$o_67SNA+aM@r+ z;Y^xii(DjaET$d&(y0&>IpOataus;fp-7l6pzXkv1FI^gExFocPM`1)D9B;#POldr zdzvNB*e1!E7Mq~)rZY7V;68yn&3Zg{uQ9MPn{fk6775{*3_3_yAE#K7p>Xe`S{bh( zwJNPlt)yrfW}}oct%AnDMT4p_+D1^MY6_Ie!SRHHqy;DdE@uEvycN~N6BH^V5v5gz zArvOFVuVVDkt#%?v1+ZTRx<@6&BcJOBnqZRB|uRC3b7G3E2c$dG^SC2XslM5Mn$M) z3XH_GT9Qy{QPK-Vk$5uevJ;@3jGf4#VW%Ut;9Y8H1bSePw z%0V`Gl8YvI)|Jh&1v*FwN-B6xsLf!YD1s*p1WyA{1XbY(ilgz_hziHlI2Ip=C~#zo zJxejRlK*8bjE^*7wCE`e2i7m~ibiKDi!K~}8+|Qcyu&1wddC7M$k7xyqL`+~wo~E-lmnP$olA*%Znkcr001*fGYlO@RN4j|8v5lr0AM zjmp5*1$IJsVk;cuOW1UN!p~S8e!>+1^z0%t;&+y=S-NJ#z>JJ%yK9!N88I*;7gzxXqA3p#jbDb$0IFV@HV&Uf_I&?S! z2*td~Y!u(|jhZ8ek5`YJ2WROplR=-Y`J|;v|AL<)W)B|ijJqM#K+ng;B=9_lCr$2q9Zpi #include #include +#include // ----------------------------------------------------------------------------- : Add card @@ -36,10 +37,53 @@ String AddCardAction::getName(bool to_undo) const { } void AddCardAction::perform(bool to_undo) { + // If we are adding cards, resolve any uid conflicts + // (If we are re-adding cards, from a remove undo, there shouldn't be any uid conflicts) + // We always assume uid conflicts occur because a card was copy-pasted into the same set, + // and never because two different cards randomly got assigned the same uid + if (action.adding && !to_undo) { + // Tally existing unique ids + unordered_map all_existing_uids; + FOR_EACH(card, set.cards) { + all_existing_uids.insert({ card->uid, card }); + } + // Tally added unique ids + unordered_map all_added_uids; + for (size_t pos = 0; pos < action.steps.size(); ++pos) { + CardP card = action.steps[pos].item; + all_added_uids.insert({ card->uid, card }); + } + FOR_EACH(added_pair, all_added_uids) { + String old_uid = added_pair.first; + CardP added_card = added_pair.second; + // Assign new unique ids + if (all_existing_uids.find(old_uid) != all_existing_uids.end()) { + String new_uid = generate_uid(); + added_card->uid = new_uid; + all_added_uids.insert({ new_uid, added_card }); + // Update links on linked cards + OTHER_LINKED_PAIRS(linked_pairs, added_card); + FOR_EACH(linked_pair, linked_pairs) { + String& linked_uid = linked_pair.first.get(); + String& linked_relation = linked_pair.second.get(); + if (linked_uid == wxEmptyString) continue; + // If it's an added card, replace the link + if (all_added_uids.find(linked_uid) != all_added_uids.end()) { + all_added_uids.at(linked_uid)->updateLink(old_uid, new_uid); + } + // Otherwise, if it's an existing card, copy the link + else if (all_existing_uids.find(linked_uid) != all_existing_uids.end()) { + all_existing_uids.at(linked_uid)->copyLink(set, old_uid, new_uid); + } + } + } + } + } + + // Add or remove cards action.perform(set.cards, to_undo); } - // ----------------------------------------------------------------------------- : Reorder cards ReorderCardsAction::ReorderCardsAction(Set& set, size_t card_id1, size_t card_id2) @@ -55,13 +99,50 @@ void ReorderCardsAction::perform(bool to_undo) { assert(card_id1 < set.cards.size()); assert(card_id2 < set.cards.size()); #endif - if (card_id1 >= set.cards.size() || card_id2 < set.cards.size()) { + if (card_id1 >= set.cards.size() || card_id2 >= set.cards.size()) { // TODO : Too lazy to fix this right now. return; } swap(set.cards[card_id1], set.cards[card_id2]); } +// ----------------------------------------------------------------------------- : Link cards + +LinkCardsAction::LinkCardsAction(Set& set, const CardP& selected_card, vector& linked_cards, const String& selected_relation, const String& linked_relation) + : CardListAction(set), selected_card(selected_card), linked_cards(linked_cards), selected_relation(selected_relation), linked_relation(linked_relation) +{} + +String LinkCardsAction::getName(bool to_undo) const { + return _("Link cards"); +} + +void LinkCardsAction::perform(bool to_undo) { + if (!to_undo) { + selected_card->link(set, linked_cards, selected_relation, linked_relation); + } else { + selected_card->unlink(linked_cards); + } +} + +UnlinkCardsAction::UnlinkCardsAction(Set& set, const CardP& selected_card, CardP& unlinked_card) + : CardListAction(set), selected_card(selected_card), unlinked_card(unlinked_card) +{} + +String UnlinkCardsAction::getName(bool to_undo) const { + return _("Unlink card"); +} + +void UnlinkCardsAction::perform(bool to_undo) { + if (!to_undo) { + pair relations = selected_card->unlink(unlinked_card); + selected_relation = relations.first; + unlinked_relation = relations.second; + } + else { + selected_card->link(set, unlinked_card, selected_relation, unlinked_relation); + } +} + // ----------------------------------------------------------------------------- : Change stylesheet String DisplayChangeAction::getName(bool to_undo) const { diff --git a/src/data/action/set.hpp b/src/data/action/set.hpp index 3b97ed94..9e43864c 100644 --- a/src/data/action/set.hpp +++ b/src/data/action/set.hpp @@ -64,6 +64,37 @@ public: const size_t card_id1, card_id2; ///< Positions of the two cards to swap }; +// ----------------------------------------------------------------------------- : Link cards + +/// Add a link between two or more cards +class LinkCardsAction : public CardListAction { +public: + LinkCardsAction(Set& set, const CardP& selected_card, vector& linked_cards, const String& selected_relation, const String& linked_relation); + + String getName(bool to_undo) const override; + void perform(bool to_undo) override; + + //private: + CardP selected_card; ///< The card currently selected in the cards tab + vector linked_cards; ///< The cards that will be linked to the selected card + String selected_relation; ///< The nature of the relation of the selected card + String linked_relation; ///< The nature of the relation of the linked cards +}; +/// Remove a link between two cards +class UnlinkCardsAction : public CardListAction { +public: + UnlinkCardsAction(Set& set, const CardP& selected_card, CardP& unlinked_card); + + String getName(bool to_undo) const override; + void perform(bool to_undo) override; + + //private: + CardP selected_card; ///< The card currently selected in the cards tab + CardP unlinked_card; ///< The card that will be unlinked from the selected card + String selected_relation; ///< The nature of the relation of the selected card + String unlinked_relation; ///< The nature of the relation of the unlinked card +}; + // ----------------------------------------------------------------------------- : Change stylesheet /// An action that affects the rendering/display/look of a set or cards in the set diff --git a/src/data/card.cpp b/src/data/card.cpp index e65b5c2a..d6ef0b80 100644 --- a/src/data/card.cpp +++ b/src/data/card.cpp @@ -8,19 +8,23 @@ #include #include +#include #include #include #include #include #include #include +#include +#include // ----------------------------------------------------------------------------- : Card Card::Card() - // for files made before we saved these times, set the time to 'yesterday' + // for files made before we saved these, set the time to 'yesterday', generate a uid : time_created (wxDateTime::Now().Subtract(wxDateSpan::Day()).ResetTime()) , time_modified(wxDateTime::Now().Subtract(wxDateSpan::Day()).ResetTime()) + , uid(generate_uid()) , has_styling(false) { if (!game_for_reading()) { @@ -32,6 +36,7 @@ Card::Card() Card::Card(const Game& game) : time_created (wxDateTime::Now()) , time_modified(wxDateTime::Now()) + , uid(generate_uid()) , has_styling(false) { data.init(game.card_fields); @@ -60,6 +65,222 @@ bool Card::contains(QuickFilterPart const& query) const { return false; } +void Card::link(const Set& set, const vector& linked_cards, const String& selected_relation, const String& linked_relation) +{ + unlink(linked_cards); + + unordered_set all_existing_uids; + FOR_EACH(card, set.cards) { + all_existing_uids.insert(card->uid); + } + int free_link_count = 0; + THIS_LINKED_PAIRS(this_linked_pairs); + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + if ( + this_linked_uid == wxEmptyString || // Not a reference + all_existing_uids.find(this_linked_uid) == all_existing_uids.end() // Reference to nonexistent card + ) free_link_count++; + } + if (free_link_count < linked_cards.size()) { + queue_message(MESSAGE_WARNING, _ERROR_("not enough free links")); + return; + } + + vector all_missed_cards; + FOR_EACH(linked_card, linked_cards) { + bool written = false; + // Try to write to a free spot + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (this_linked_uid == wxEmptyString) { + this_linked_uid = linked_card->uid; + this_linked_relation = linked_relation; + written = true; + break; + } + } + // Try to write to an erasable spot + if (!written) { + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (all_existing_uids.find(this_linked_uid) == all_existing_uids.end()) { + this_linked_uid = linked_card->uid; + this_linked_relation = linked_relation; + written = true; + break; + } + } + } + if (!written) { + // Should be impossible to end up here? + } + + OTHER_LINKED_PAIRS(linked_pairs, linked_card); + written = false; + // Try to write to a free spot + FOR_EACH(linked_pair, linked_pairs) { + String& linked_uid = linked_pair.first.get(); + String& linked_relation = linked_pair.second.get(); + if (linked_uid == wxEmptyString) { + linked_uid = uid; + linked_relation = selected_relation; + written = true; + break; + } + } + // Try to write to an erasable spot + if (!written) { + FOR_EACH(linked_pair, linked_pairs) { + String& linked_uid = linked_pair.first.get(); + String& linked_relation = linked_pair.second.get(); + if (all_existing_uids.find(linked_uid) == all_existing_uids.end()) { + linked_uid = uid; + linked_relation = selected_relation; + written = true; + break; + } + } + } + // Notify we couldn't write + if (!written) { + all_missed_cards.push_back(linked_card); + } + } + if (all_missed_cards.size() > 0) { + std::stringstream ss; + ss << _ERROR_("could not link"); + for (size_t pos = 0; pos < all_missed_cards.size(); ++pos) { + ss << all_missed_cards[pos]->identification(); + if (pos < all_missed_cards.size() - 1) ss << ", "; + }; + String wxString(ss.str().c_str(), wxConvUTF8); + queue_message(MESSAGE_WARNING, wxString); + } +} + +void Card::link(const Set& set, CardP& linked_card, const String& selected_relation, const String& linked_relation) +{ + vector linked_cards { linked_card }; + link(set, linked_cards, selected_relation, linked_relation); +} + +void Card::unlink(const vector& unlinked_cards) +{ + for (size_t pos = 0; pos < unlinked_cards.size(); ++pos) { + CardP unlinked_card = unlinked_cards[pos]; + unlink(unlinked_card); + } +} + +pair Card::unlink(CardP& unlinked_card) +{ + String old_selected_relation = wxEmptyString; + THIS_LINKED_PAIRS(this_linked_pairs); + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (this_linked_uid == unlinked_card->uid) { + old_selected_relation = this_linked_relation; + this_linked_uid = wxEmptyString; + this_linked_relation = wxEmptyString; + } + } + String old_unlinked_relation = wxEmptyString; + OTHER_LINKED_PAIRS(unlinked_pairs, unlinked_card); + FOR_EACH(unlinked_pair, unlinked_pairs) { + String& unlinked_uid = unlinked_pair.first.get(); + String& unlinked_relation = unlinked_pair.second.get(); + if (unlinked_uid == uid) { + old_unlinked_relation = unlinked_relation; + unlinked_uid = wxEmptyString; + unlinked_relation = wxEmptyString; + } + } + return make_pair(old_selected_relation, old_unlinked_relation); +} + +void Card::copyLink(const Set& set, String old_uid, String new_uid) { + // Find what relation we need to copy + String relation_copy = wxEmptyString; + THIS_LINKED_PAIRS(this_linked_pairs); + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (this_linked_uid == old_uid) { + relation_copy = this_linked_relation; + break; + } + } + // Nothing to copy + if (relation_copy == wxEmptyString) { + return; + } + + // Try to copy to a free spot + bool written = false; + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (this_linked_uid == wxEmptyString) { + this_linked_uid = new_uid; + this_linked_relation = relation_copy; + written = true; + break; + } + } + // Try to copy to an erasable spot + if (!written) { + unordered_set all_existing_uids; + FOR_EACH(card, set.cards) { + all_existing_uids.insert(card->uid); + } + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + String& this_linked_relation = this_linked_pair.second.get(); + if (all_existing_uids.find(this_linked_uid) == all_existing_uids.end()) { + this_linked_uid = new_uid; + this_linked_relation = relation_copy; + written = true; + break; + } + } + } + // Notify we couldn't copy + if (!written) { + queue_message(MESSAGE_WARNING, _ERROR_("not enough free links for copy")); + } +} + +void Card::updateLink(String old_uid, String new_uid) { + THIS_LINKED_PAIRS(this_linked_pairs); + FOR_EACH(this_linked_pair, this_linked_pairs) { + String& this_linked_uid = this_linked_pair.first.get(); + if (this_linked_uid == old_uid) { + this_linked_uid = new_uid; + return; + } + } +} + +vector> Card::getLinkedCards(const Set& set) { + unordered_map links{ + { linked_card_1, linked_relation_1 }, + { linked_card_2, linked_relation_2 }, + { linked_card_3, linked_relation_3 }, + { linked_card_4, linked_relation_4 } + }; + vector> linked_cards; + FOR_EACH(other_card, set.cards) { + if (links.find(other_card->uid) != links.end()) { + linked_cards.push_back(make_pair(other_card, links.at(other_card->uid))); + } + } + return linked_cards; +} + IndexMap& Card::extraDataFor(const StyleSheet& stylesheet) { return extra_data.get(stylesheet.name(), stylesheet.extra_card_fields); } @@ -89,6 +310,15 @@ IMPLEMENT_REFLECTION(Card) { } } REFLECT(notes); + REFLECT(uid); + REFLECT(linked_card_1); + REFLECT(linked_card_2); + REFLECT(linked_card_3); + REFLECT(linked_card_4); + REFLECT(linked_relation_1); + REFLECT(linked_relation_2); + REFLECT(linked_relation_3); + REFLECT(linked_relation_4); REFLECT(time_created); REFLECT(time_modified); REFLECT(extra_data); // don't allow scripts to depend on style specific data diff --git a/src/data/card.hpp b/src/data/card.hpp index 7d6f0947..f5170b73 100644 --- a/src/data/card.hpp +++ b/src/data/card.hpp @@ -17,11 +17,15 @@ class Game; class Dependency; class Keyword; +DECLARE_POINTER_TYPE(Set); DECLARE_POINTER_TYPE(Card); DECLARE_POINTER_TYPE(Field); DECLARE_POINTER_TYPE(Value); DECLARE_POINTER_TYPE(StyleSheet); +#define THIS_LINKED_PAIRS(var) vector, reference_wrapper>> var { make_pair(ref(linked_card_1), ref(linked_relation_1)), make_pair(ref(linked_card_2), ref(linked_relation_2)), make_pair(ref(linked_card_3), ref(linked_relation_3)), make_pair(ref(linked_card_4), ref(linked_relation_4)) } +#define OTHER_LINKED_PAIRS(var, other_card) vector, reference_wrapper>> var { make_pair(ref(other_card->linked_card_1), ref(other_card->linked_relation_1)), make_pair(ref(other_card->linked_card_2), ref(other_card->linked_relation_2)), make_pair(ref(other_card->linked_card_3), ref(other_card->linked_relation_3)), make_pair(ref(other_card->linked_card_4), ref(other_card->linked_relation_4)) } + // ----------------------------------------------------------------------------- : Card /// A card from a card Set @@ -37,6 +41,18 @@ public: IndexMap data; /// Notes for this card String notes; + /// Unique identifier for this card, so other cards can refer to it, and be linked to it + String uid; + /// Up to four uid of other cards, to encode relations such as front face/back face, or generator/token, etc... + String linked_card_1; + String linked_card_2; + String linked_card_3; + String linked_card_4; + /// Nature of the relatation with the respective linked card, such as back face, or token, etc... + String linked_relation_1; + String linked_relation_2; + String linked_relation_3; + String linked_relation_4; /// Time the card was created/last modified wxDateTime time_created, time_modified; /// Alternative style to use for this card @@ -64,6 +80,17 @@ public: /// Does any field contains the given query string? bool contains(QuickFilterPart const& query) const; + /// Link or unlink other cards to this card + void link(const Set& set, const vector& linked_cards, const String& selected_relation, const String& linked_relation); + void link(const Set& set, CardP& linked_card, const String& selected_relation, const String& linked_relation); + void unlink(const vector& linked_cards); + pair unlink(CardP& unlinked_card); // Returns the relations that were deleted, so we can undo + + void copyLink(const Set& set, String old_uid, String new_uid); + void updateLink(String old_uid, String new_uid); + + vector> getLinkedCards(const Set& set); + /// Find a value in the data by name and type template T& value(const String& name) { for(IndexMap::iterator it = data.begin() ; it != data.end() ; ++it) { diff --git a/src/data/field.cpp b/src/data/field.cpp index e0f5c4fc..c4805977 100644 --- a/src/data/field.cpp +++ b/src/data/field.cpp @@ -68,14 +68,14 @@ IMPLEMENT_REFLECTION(Field) { } void Field::after_reading(Version ver) { - name = canonical_name_form(name); - if(caption.default_.empty()) caption.default_ = tr(package_relative_filename, name, name_to_caption); + name = canonical_name_form(name); + if(caption.default_.empty()) caption.default_ = tr(package_relative_filename, name, name_to_caption); if(card_list_name.default_.empty()) card_list_name.default_ = tr(package_relative_filename, caption.default_, capitalize); } template <> intrusive_ptr read_new(Reader& reader) { - intrusive_ptr field; + intrusive_ptr field; // there must be a type specified String type; reader.handle(_("type"), type); @@ -95,7 +95,7 @@ intrusive_ptr read_new(Reader& reader) { } else { reader.warning(_ERROR_1_("unsupported field type", type)); throw ParseError(_ERROR_("aborting parsing")); - } + } field->package_relative_filename = reader.getPackage()->relativeFilename().Clone(); return field; } @@ -222,8 +222,38 @@ void Style::checkContentDependencies(Context& ctx, const Dependency& dep) const void Style::markDependencyMember(const String& name, const Dependency& dep) const { // mark dependencies on content - if (dep.type == DEP_DUMMY && dep.index == false && (starts_with(name, _("content")) || name == "layout") ) { - // anything that starts with "content_" is a content property + if ( + dep.type == DEP_DUMMY && dep.index == false && ( + starts_with(name, _("content")) || + name == "layout" || + name == "lines" || + name == "paragraphs" || + name == "blocks" || + name == "separators" || + name == "font" || + name == "symbol_font" || + name == "always_symbol" || + name == "allow_formating" || + name == "alignment" || + name == "padding_left" || + name == "padding_right" || + name == "padding_top" || + name == "padding_bottom" || + name == "padding_left_min" || + name == "padding_right_min" || + name == "padding_top_min" || + name == "padding_bottom_min" || + name == "line_height_soft" || + name == "line_height_hard" || + name == "line_height_line" || + name == "line_height_soft_max" || + name == "line_height_hard_max" || + name == "line_height_line_max" || + name == "paragraph_height" || + name == "block_height_min" || + name == "direction" + ) + ) { const_cast(dep).index = true; } } diff --git a/src/data/field.hpp b/src/data/field.hpp index 23db0ae7..2f25bbf3 100644 --- a/src/data/field.hpp +++ b/src/data/field.hpp @@ -61,7 +61,7 @@ public: Alignment card_list_align; ///< Alignment of the card list colummn. OptionalScript sort_script; ///< The script to use when sorting this, if not the value. OptionalScript import_script; ///< The script to apply to the supplied value, when creating a new card. - Dependencies dependent_scripts; ///< Scripts that depend on values of this field + Dependencies dependent_scripts; ///< Scripts that depend on values of this field String package_relative_filename; /// Creates a new Value corresponding to this Field diff --git a/src/data/font.cpp b/src/data/font.cpp index 82be6083..fa16ed1d 100644 --- a/src/data/font.cpp +++ b/src/data/font.cpp @@ -8,10 +8,10 @@ #include #include -#include -#include -#include - +#include +#include +#include + // ----------------------------------------------------------------------------- : Font Font::Font() @@ -27,59 +27,59 @@ Font::Font() , separator_color(Color(0,0,0,128)) , flags(FONT_NORMAL) {} - -bool Font::PreloadResourceFonts(bool recursive) { -#if wxUSE_PRIVATE_FONTS - String pathSeparator(wxFileName::GetPathSeparator()); - String appPath(wxFileName(wxStandardPaths::Get().GetExecutablePath()).GetPath()); - wxDir appDir(appPath); - if (!appDir.IsOpened()) return true; - bool preloadHadErrors = false; +bool Font::PreloadResourceFonts(bool recursive) { +#if wxUSE_PRIVATE_FONTS + String pathSeparator(wxFileName::GetPathSeparator()); + String appPath(wxFileName(wxStandardPaths::Get().GetExecutablePath()).GetPath()); + wxDir appDir(appPath); + if (!appDir.IsOpened()) return true; + + bool preloadHadErrors = false; wxString folder; bool cont = appDir.GetFirst(&folder, wxEmptyString, wxDIR_DIRS); while (cont) - { + { if (folder.Lower().Contains("fonts")) { - String folderPath = appPath + pathSeparator + folder + pathSeparator; - - // tally fonts - vector fontFilePaths; - TallyResourceFonts(folderPath, fontFilePaths, recursive); - - // load fonts - for (const String& fontFilePath : fontFilePaths) { - if (!wxFont::AddPrivateFont(fontFilePath)) { - preloadHadErrors = true; - } + String folderPath = appPath + pathSeparator + folder + pathSeparator; + + // tally fonts + vector fontFilePaths; + TallyResourceFonts(folderPath, fontFilePaths, recursive); + + // load fonts + for (const String& fontFilePath : fontFilePaths) { + if (!wxFont::AddPrivateFont(fontFilePath)) { + preloadHadErrors = true; + } } - } + } cont = appDir.GetNext(&folder); - } - - return preloadHadErrors; - -#endif // wxUSE_PRIVATE_FONTS - return false; -} - -void Font::TallyResourceFonts(String fontsDirectoryPath, vector& fontFilePaths, bool recursive) { - wxDir fontsDirectory(fontsDirectoryPath); - String fontFileName = wxEmptyString; - bool hasNext = fontsDirectory.GetFirst(&fontFileName); - while (hasNext) { - String fontFilePath = fontsDirectoryPath + fontFileName; - if (wxDirExists(fontFilePath)) { - if (recursive) { - TallyResourceFonts(fontFilePath + wxFileName::GetPathSeparator(), fontFilePaths, true); - } - } - else if (fontFilePath.EndsWith(_(".ttf")) || fontFilePath.EndsWith(_(".otf"))) { - fontFilePaths.push_back(fontFilePath); - } - hasNext = fontsDirectory.GetNext(&fontFileName); - } -} + } + + return preloadHadErrors; + +#endif // wxUSE_PRIVATE_FONTS + return false; +} + +void Font::TallyResourceFonts(String fontsDirectoryPath, vector& fontFilePaths, bool recursive) { + wxDir fontsDirectory(fontsDirectoryPath); + String fontFileName = wxEmptyString; + bool hasNext = fontsDirectory.GetFirst(&fontFileName); + while (hasNext) { + String fontFilePath = fontsDirectoryPath + fontFileName; + if (wxDirExists(fontFilePath)) { + if (recursive) { + TallyResourceFonts(fontFilePath + wxFileName::GetPathSeparator(), fontFilePaths, true); + } + } + else if (fontFilePath.EndsWith(_(".ttf")) || fontFilePath.EndsWith(_(".otf"))) { + fontFilePaths.push_back(fontFilePath); + } + hasNext = fontsDirectory.GetNext(&fontFileName); + } +} bool Font::update(Context& ctx) { bool changes = false; diff --git a/src/data/font.hpp b/src/data/font.hpp index 151202fa..6fa6ddc7 100644 --- a/src/data/font.hpp +++ b/src/data/font.hpp @@ -46,15 +46,16 @@ public: Scriptable shadow_displacement_y;///< Position of the shadow Scriptable shadow_blur; ///< Blur radius of the shadow Color separator_color; ///< Color for text - int flags; ///< FontFlags for this font - + int flags; ///< FontFlags for this font + Font(); - - /// Load fonts (.ttf or .otf) from all directories in the app directory that contain "fonts" in their names, - /// and optionaly their subdirectories, returns true if there were errors - static bool PreloadResourceFonts(bool recursive); - /// Adds font file paths from the given directory into fontFilePaths - static void TallyResourceFonts(String fontsDirectoryPath, vector& fontFilePaths, bool recursive); + + /// Load fonts (.ttf or .otf) from all directories in the app directory that contain "fonts" in their names, + /// and optionaly their subdirectories, returns true if there were errors + static bool PreloadResourceFonts(bool recursive); + /// Adds font file paths from the given directory into fontFilePaths + static void TallyResourceFonts(String fontsDirectoryPath, vector& fontFilePaths, bool recursive); + /// Update the scritables, returns true if there is a change bool update(Context& ctx); /// Add the given dependency to the dependent_scripts list for the variables this font depends on @@ -70,7 +71,7 @@ public: /// Convert this font to a wxFont wxFont toWxFont(double scale) const; - + private: DECLARE_REFLECTION(); }; diff --git a/src/data/format/clipboard.cpp b/src/data/format/clipboard.cpp index 6574cfbc..01fa12f0 100644 --- a/src/data/format/clipboard.cpp +++ b/src/data/format/clipboard.cpp @@ -147,6 +147,9 @@ CardsOnClipboard::CardsOnClipboard(const SetP& set, const vector& cards) if (cards.size() == 1) { Add(new wxBitmapDataObject(export_bitmap(set, cards[0]))); } + else if (cards.size() < 6) { + Add(new wxBitmapDataObject(export_bitmap(set, cards, true, 0, 1.0, 0.0))); + } // Conversion to serialized card format Add(new CardsDataObject(set, cards), true); } diff --git a/src/data/format/formats.hpp b/src/data/format/formats.hpp index fd7d9665..d6885be3 100644 --- a/src/data/format/formats.hpp +++ b/src/data/format/formats.hpp @@ -99,11 +99,12 @@ void export_images(const SetP& set, const vector& cards, void export_image(const SetP& set, const CardP& card, const String& filename); /// Generate a bitmap image of a card -Bitmap export_bitmap(const SetP& set, const CardP& card); +Bitmap export_bitmap(const SetP& set, const CardP& card); Bitmap export_bitmap(const SetP& set, const CardP& card, const double zoom, const Radians angle_radians); +Bitmap export_bitmap(const SetP& set, const vector& cards, bool scale_to_lowest_dpi, int padding, const double zoom, const Radians angle_radians); /// Export a set to Magic Workstation format void export_mws(Window* parent, const SetP& set); /// Export a set to Apprentice -void export_apprentice(Window* parent, const SetP& set); +void export_apprentice(Window* parent, const SetP& set); diff --git a/src/data/format/image.cpp b/src/data/format/image.cpp index 0083a03c..03513763 100644 --- a/src/data/format/image.cpp +++ b/src/data/format/image.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -25,47 +26,47 @@ void export_image(const SetP& set, const CardP& card, const String& filename) { } class UnzoomedDataViewer : public DataViewer { -public: - UnzoomedDataViewer(); +public: + UnzoomedDataViewer(); UnzoomedDataViewer(double zoom, double angle); virtual ~UnzoomedDataViewer() {}; Rotation getRotation() const override; -private: +private: double zoom; - double angle; + double angle; bool declared_values; -}; - -UnzoomedDataViewer::UnzoomedDataViewer() - : zoom(1.0) - , angle(0.0) - , declared_values(false) -{} - -UnzoomedDataViewer::UnzoomedDataViewer(const double zoom, const Radians angle = 0.0) - : zoom(zoom) - , angle(angle) - , declared_values(true) +}; + +UnzoomedDataViewer::UnzoomedDataViewer() + : zoom(1.0) + , angle(0.0) + , declared_values(false) +{} + +UnzoomedDataViewer::UnzoomedDataViewer(const double zoom, const Radians angle = 0.0) + : zoom(zoom) + , angle(angle) + , declared_values(true) {} Rotation UnzoomedDataViewer::getRotation() const { - if (!stylesheet) stylesheet = set->stylesheet; - if (declared_values) { - return Rotation(angle, stylesheet->getCardRect(), zoom, 1.0, ROTATION_ATTACH_TOP_LEFT); - } + if (!stylesheet) stylesheet = set->stylesheet; + if (declared_values) { + return Rotation(angle, stylesheet->getCardRect(), zoom, 1.0, ROTATION_ATTACH_TOP_LEFT); + } - double export_zoom = settings.stylesheetSettingsFor(set->stylesheetFor(card)).export_zoom(); - bool use_viewer_rotation = !settings.stylesheetSettingsFor(set->stylesheetFor(card)).card_normal_export(); - - if (use_viewer_rotation) { + double export_zoom = settings.stylesheetSettingsFor(set->stylesheetFor(card)).export_zoom(); + bool use_viewer_rotation = !settings.stylesheetSettingsFor(set->stylesheetFor(card)).card_normal_export(); + + if (use_viewer_rotation) { return Rotation(DataViewer::getRotation().getAngle(), stylesheet->getCardRect(), export_zoom, 1.0, ROTATION_ATTACH_TOP_LEFT); } else { return Rotation(angle, stylesheet->getCardRect(), export_zoom, 1.0, ROTATION_ATTACH_TOP_LEFT); } -} - -Bitmap export_bitmap(const SetP& set, const CardP& card) { - if (!set) throw Error(_("no set")); +} + +Bitmap export_bitmap(const SetP& set, const CardP& card) { + if (!set) throw Error(_("no set")); UnzoomedDataViewer viewer = UnzoomedDataViewer(); viewer.setSet(set); viewer.setCard(card); @@ -79,11 +80,11 @@ Bitmap export_bitmap(const SetP& set, const CardP& card) { // draw viewer.draw(dc); dc.SelectObject(wxNullBitmap); - return bitmap; -} - -Bitmap export_bitmap(const SetP& set, const CardP& card, const double zoom, const Radians angle = 0.0) { - if (!set) throw Error(_("no set")); + return bitmap; +} + +Bitmap export_bitmap(const SetP& set, const CardP& card, const double zoom, const Radians angle = 0.0) { + if (!set) throw Error(_("no set")); UnzoomedDataViewer viewer = UnzoomedDataViewer(zoom, angle); viewer.setSet(set); viewer.setCard(card); @@ -97,8 +98,59 @@ Bitmap export_bitmap(const SetP& set, const CardP& card, const double zoom, cons // draw viewer.draw(dc); dc.SelectObject(wxNullBitmap); - return bitmap; -} + return bitmap; +} + +// put multiple card images into one bitmap +Bitmap export_bitmap(const SetP& set, const vector& cards, bool scale_to_lowest_dpi, int padding, const double zoom, const Radians angle = 0.0) { + if (!set) throw Error(_("no set")); + vector bitmaps; + int width = 0; + int height = 0; + double lowest_dpi = 1200.0; + if (scale_to_lowest_dpi) { + FOR_EACH(card, cards) { + lowest_dpi = min(lowest_dpi, set->stylesheetFor(card).card_dpi); + } + lowest_dpi = max(lowest_dpi, 150.0); + } + // Draw card bitmaps + FOR_EACH(card, cards) { + double scaled_zoom = zoom; + if (scale_to_lowest_dpi) { + double dpi = max(set->stylesheetFor(card).card_dpi, 150.0); + scaled_zoom *= lowest_dpi / dpi; + } + UnzoomedDataViewer viewer = UnzoomedDataViewer(scaled_zoom, angle); + viewer.setSet(set); + viewer.setCard(card); + RealSize size = viewer.getRotation().getExternalSize(); + Bitmap bitmap((int)size.width, (int)size.height); + if (!bitmap.Ok()) throw InternalError(_("Unable to create bitmap")); + wxMemoryDC bufferDC; + bufferDC.SelectObject(bitmap); + clearDC(bufferDC, *wxWHITE_BRUSH); + viewer.draw(bufferDC); + bufferDC.SelectObject(wxNullBitmap); + width += (int)size.width; + height = max(height, (int)size.height); + bitmaps.push_back(bitmap); + } + // Draw global bitmap + Bitmap global_bitmap(width + (bitmaps.size()-1) * padding, height); + if (!global_bitmap.Ok()) throw InternalError(_("Unable to create bitmap")); + wxMemoryDC globalDC; + globalDC.SelectObject(global_bitmap); + clearDC(globalDC, *wxWHITE_BRUSH); + int offset = 0; + FOR_EACH(bitmap, bitmaps) { + globalDC.SetDeviceOrigin(offset, 0); + globalDC.DrawBitmap(bitmap, 0, 0); + offset += bitmap.GetWidth() + padding; + } + globalDC.SelectObject(wxNullBitmap); + return global_bitmap; +} // ----------------------------------------------------------------------------- : Multiple card export diff --git a/src/data/game.cpp b/src/data/game.cpp index b588bba1..6b2a9deb 100644 --- a/src/data/game.cpp +++ b/src/data/game.cpp @@ -48,6 +48,7 @@ IMPLEMENT_REFLECTION(Game) { } REFLECT_NO_SCRIPT(default_set_style); REFLECT_NO_SCRIPT(card_fields); + REFLECT_NO_SCRIPT(card_links); REFLECT_NO_SCRIPT(card_list_color_script); REFLECT_NO_SCRIPT(import_script); REFLECT_NO_SCRIPT(json_paths); @@ -95,26 +96,26 @@ void Game::validate(Version v) { pack->filter = OptionalScript(_("true")); pack->select = SELECT_NO_REPLACE; pack_types.push_back(pack); - } - // alternate card field names map + } + // alternate card field names map for (auto it = card_fields.begin(); it != card_fields.end(); ++it) { - FieldP field = *it; - String unified_name = unified_form(field->name); - if (card_fields_alt_names.count(unified_name)) { - queue_message(MESSAGE_WARNING, _("Duplicate alternate card field name: ") + unified_name); - } - else { - card_fields_alt_names.emplace(unified_name, field->name); - } - //String column_name = field->card_list_name.get(); - //card_fields_alt_names.emplace(unified_form(column_name), field->name); - for (auto it2 = field->alt_names.begin(); it2 != field->alt_names.end(); ++it2) { - unified_name = unified_form(*it2); - if (card_fields_alt_names.count(unified_name)) { - queue_message(MESSAGE_WARNING, _("Duplicate alternate card field name: ") + unified_name); - } - else { - card_fields_alt_names.emplace(unified_name, field->name); + FieldP field = *it; + String unified_name = unified_form(field->name); + if (card_fields_alt_names.count(unified_name)) { + queue_message(MESSAGE_WARNING, _("Duplicate alternate card field name: ") + unified_name); + } + else { + card_fields_alt_names.emplace(unified_name, field->name); + } + //String column_name = field->card_list_name.get(); + //card_fields_alt_names.emplace(unified_form(column_name), field->name); + for (auto it2 = field->alt_names.begin(); it2 != field->alt_names.end(); ++it2) { + unified_name = unified_form(*it2); + if (card_fields_alt_names.count(unified_name)) { + queue_message(MESSAGE_WARNING, _("Duplicate alternate card field name: ") + unified_name); + } + else { + card_fields_alt_names.emplace(unified_name, field->name); } } } @@ -138,7 +139,7 @@ void Game::initCardListColorScript() { return; } } -} +} // special behaviour of reading/writing GamePs: only read/write the name diff --git a/src/data/game.hpp b/src/data/game.hpp index 43852209..5b422573 100644 --- a/src/data/game.hpp +++ b/src/data/game.hpp @@ -12,7 +12,7 @@ #include #include