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 00000000..67ac5a48
Binary files /dev/null and b/resource/tool/card_copy.png differ
diff --git a/resource/tool/card_link.png b/resource/tool/card_link.png
new file mode 100644
index 00000000..087a44c5
Binary files /dev/null and b/resource/tool/card_link.png differ
diff --git a/resource/win32_res.rc b/resource/win32_res.rc
index b7e3d252..0679722b 100644
--- a/resource/win32_res.rc
+++ b/resource/win32_res.rc
@@ -47,6 +47,8 @@ tool/no_auto IMAGE "tool/no_auto.png"
tool/card_add IMAGE "tool/card_add.png"
tool/card_add_multiple IMAGE "tool/card_add_multiple.png"
tool/card_del IMAGE "tool/card_del.png"
+tool/card_link IMAGE "tool/card_link.png"
+tool/card_copy IMAGE "tool/card_copy.png"
tool/card_rotate IMAGE "tool/card_rotate.png"
tool/card_rotate_0 IMAGE "tool/card_rotate_0.png"
tool/card_rotate_90 IMAGE "tool/card_rotate_90.png"
diff --git a/src/data/action/set.cpp b/src/data/action/set.cpp
index e0d349fd..57592d62 100644
--- a/src/data/action/set.cpp
+++ b/src/data/action/set.cpp
@@ -13,6 +13,7 @@
#include
#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