pax_global_header00006660000000000000000000000064152652161200014512gustar00rootroot0000000000000052 comment=9eac83f49130268c7fda760582238797d810b336 trizbort-qt-1.0.1/000077500000000000000000000000001526521612000140125ustar00rootroot00000000000000trizbort-qt-1.0.1/CMakeLists.txt000066400000000000000000000127051526521612000165570ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2026 Jason Self # SPDX-License-Identifier: GPL-3.0-or-later # # Copyright (c) 2026 Jason Self # # This file is part of Trizbort-qt. # # Trizbort-qt 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 3 of the License, or (at your option) any later # version. # # Trizbort-qt 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 # Trizbort-qt. If not, see . cmake_minimum_required(VERSION 3.16) project(trizbort-qt VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) # compile help/help.qrc (built-in help) into the binary # Bake the program version into a generated Version.h that the About box and the # --version flag read. The version is declared once in project() above. # Packagers can override it with -DTRIZBORT_VERSION=; the Debian packaging # passes the changelog's upstream version this way, keeping the .deb version # authoritative and the build independent of git. if(DEFINED TRIZBORT_VERSION AND NOT "${TRIZBORT_VERSION}" STREQUAL "") set(TRIZBORT_VERSION_STRING "${TRIZBORT_VERSION}") else() set(TRIZBORT_VERSION_STRING "${PROJECT_VERSION}") endif() message(STATUS "Trizbort version: ${TRIZBORT_VERSION_STRING}") configure_file(src/Version.h.in "${CMAKE_CURRENT_BINARY_DIR}/generated/Version.h" @ONLY) find_package(Qt6 REQUIRED COMPONENTS Widgets Network Svg) add_executable(trizbort-qt help/help.qrc packaging/app_icon.qrc # embed the app icon SVG (see main.cpp setWindowIcon) src/HelpWindow.cpp src/main.cpp src/MapDocument.cpp src/DotNetColor.cpp src/TrizbortReader.cpp src/TrizbortWriter.cpp src/TranscriptAutomapper.cpp src/MapScene.cpp src/MapView.cpp src/MapRender.cpp src/MainWindow.cpp src/RoomItem.cpp src/ConnectionItem.cpp src/EditCommands.cpp src/RoomDialog.cpp src/ConnectionDialog.cpp src/SettingsDialog.cpp src/export/ExportModel.cpp src/export/ExporterFactory.cpp src/export/CodeExporter.cpp src/export/ZilExporter.cpp src/export/AlanExporter.cpp src/export/Inform6Exporter.cpp src/export/Inform7Exporter.cpp src/export/QuestExporter.cpp ) target_include_directories(trizbort-qt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated") target_link_libraries(trizbort-qt PRIVATE Qt6::Widgets Qt6::Network Qt6::Svg) # Platform-native application icon. # # The embedded SVG above (app_icon.qrc) gives the running program its # window/taskbar/dock icon on every platform: setWindowIcon renders it on demand # via Qt's SVG icon engine (hence the Qt6::Svg link). In addition, Windows and # macOS want the icon baked into the executable/bundle itself # The .ico and .icns are committed beside the SVG they are generated from # (packaging/generate-icons.sh) so a from-source build on any platform needs no # image tooling. if(WIN32) # Compile a .rc that points the resource compiler at the .ico. The path is # made absolute at configure time so rc.exe finds it regardless of its cwd. set(TRIZBORT_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/packaging/trizbort-qt.ico") configure_file(packaging/trizbort-qt.rc.in "${CMAKE_CURRENT_BINARY_DIR}/trizbort-qt.rc" @ONLY) target_sources(trizbort-qt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/trizbort-qt.rc") elseif(APPLE) # Build a .app bundle and drop the .icns into it as the bundle icon. set(TRIZBORT_ICNS "${CMAKE_CURRENT_SOURCE_DIR}/packaging/trizbort-qt.icns") target_sources(trizbort-qt PRIVATE "${TRIZBORT_ICNS}") set_source_files_properties("${TRIZBORT_ICNS}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") set_target_properties(trizbort-qt PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_BUNDLE_NAME "Trizbort-qt" MACOSX_BUNDLE_GUI_IDENTIFIER "org.jxself.trizbort-qt" MACOSX_BUNDLE_ICON_FILE "trizbort-qt.icns" MACOSX_BUNDLE_BUNDLE_VERSION "${TRIZBORT_VERSION_STRING}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${TRIZBORT_VERSION_STRING}") endif() # Installation (FHS layout via GNUInstallDirs; used by `cmake --install` and by # the Debian packaging). include(GNUInstallDirs) # RUNTIME covers the plain executable (GNU/Linux. Windows and the .deb); BUNDLE # covers the macOS .app when MACOSX_BUNDLE is set above. CMake requires a # BUNDLE DESTINATION as soon as the target can be a bundle, even though # macOS binaries are produced with macdeployqt rather than `cmake --install`. install(TARGETS trizbort-qt RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES packaging/trizbort-qt.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES packaging/trizbort-qt.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) install(FILES packaging/trizbort-qt.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages) install(FILES packaging/org.jxself.trizbort-qt.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(FILES packaging/trizbort-qt.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) trizbort-qt-1.0.1/COPYING000066400000000000000000001045151526521612000150530ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 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 state 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 3 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, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU 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 Lesser General Public License instead of this License. But first, please read . trizbort-qt-1.0.1/README.md000066400000000000000000000106021526521612000152700ustar00rootroot00000000000000# trizbort-qt Trizbort-qt is a cross-platform port of Trizbort, a mapper for interactive fiction and text adventures. It draws rooms and the connections between them, annotates them with descriptions and objects, and exports the map as source code for many interactive fiction systems (ZIL, Inform 6 and 7, Alan and Quest) or as a PDF or image. It can also build a map automatically from a play-session transcript, following the player's movements, and it reads and writes the .trizbort file format used by the original Trizbort. If you prefer working inside a web browser instead of running a local app, check out Trizbort.io. ## Building ### GNU/Linux Requires Qt 6 (Widgets and Network) and CMake ≥ 3.16: ```sh sudo apt install qt6-base-dev cmake g++ cmake -S . -B build cmake --build build -j ``` ### Building on macOS or Windows This is possible but I don't have a Windows or Mac box to do so. If you'd like to contribute binary builds for these, please let me know. ### Command line With no options (or just a map file) trizbort-qt opens the editor: ```sh trizbort-qt # empty editor trizbort-qt map.trizbort # open a map in the editor trizbort-qt -a # reopen the most recent map # (--loadlastproject) ``` It also has a set of headless batch modes, each of which does one job and exits without opening a window — handy for scripts, Makefiles, and CI. They print progress to standard output and exit non-zero on failure. ```sh # Re-save / round-trip trizbort-qt map.trizbort --save out.trizbort # load and re-save to a new file trizbort-qt map.trizbort -q # re-save in place (--quicksave) trizbort-qt map.trizbort -s # re-save, plus write a PDF and/or # image alongside it, per the # Smart Save preferences # (--smartsave) # Render trizbort-qt map.trizbort --render out.png # render the map to an image trizbort-qt map.trizbort --pdf out.pdf # render the map to PDF # Statistics trizbort-qt map.trizbort --stats # print a map statistics report # Export to interactive-fiction source trizbort-qt map.trizbort --zil out.zil # ZIL (Infocom / ZILF) trizbort-qt map.trizbort --inform7 out.ni # Inform 7 trizbort-qt map.trizbort --inform6 out.inf # Inform 6 trizbort-qt map.trizbort --alan out.i # Alan trizbort-qt map.trizbort --quest out.aslx # Quest 5 trizbort-qt map.trizbort --questrooms out.aslx # Quest 5 (rooms only) # Build a map from a play transcript trizbort-qt --import-transcript play.txt out.trizbort # Print the version and exit trizbort-qt --version # (-v) ``` `-x` / `--exit` starts up and exits immediately without opening the editor, mirroring the C# version's flag. ## Bugs & Feature Requests If something isn't working right, report it by email to . Feature requests are just as helpful. The big thing is: please include your exact version number whenever you report a bug. You can find it in the About box in the menu, or run trizbort-qt --version in your terminal if you prefer the command line. You should also include the steps to reproduce it. Not having to guess makes fixing things so much easier. ## Licensing The original C# version of Trizbort is under the MIT License. The porting and modifications are licensed under the GPL-3.0-or-later, making the effective license for the C++/Qt port GPL-3.0-or-later but check individual files for details. This file is part of Trizbort-qt. Trizbort-qt 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 3 of the License, or (at your option) any later version. Trizbort-qt 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 Trizbort-qt. If not, see . SPDX-FileCopyrightText: 2026 Jason Self SPDX-License-Identifier: GPL-3.0-or-later trizbort-qt-1.0.1/help/000077500000000000000000000000001526521612000147425ustar00rootroot00000000000000trizbort-qt-1.0.1/help/01-introduction.html000066400000000000000000000055271526521612000206000ustar00rootroot00000000000000 Introduction and the main window - Trizbort-qt Help

§1 Introduction and the main window

This section describes the parts of the Trizbort-qt main window. Trizbort-qt is a visual mapping tool for interactive fiction: it draws a map of rooms joined by connections and exports it as an image, a PDF, or source code for a range of interactive-fiction systems.

The map canvas

Most of the window is the map canvas, on which rooms and connections are drawn. Hovering over a room or connection shows a tooltip with its name and other details.

The menu bar has nine menus: File, Edit, Rooms, Connections, Automap, Validation, Tools, View, and Help. Rooms are covered in §2 (Working with rooms) and connections in §3 (Working with connections).

The toolbar

A small toolbar holds two connection toggles: Toggle Dotted Lines and Toggle Directional Lines. Each flips the style, respectively the flow, of the selected connections.

The Mini Map

A Mini Map dock gives an overview of the whole map. It is hidden until it is shown from the View menu's Mini Map item.

The status bar

A status bar along the bottom shows brief messages, and at its right the current zoom level as a percentage.

trizbort-qt-1.0.1/help/02-rooms.html000066400000000000000000000156031526521612000172130ustar00rootroot00000000000000 Working with rooms - Trizbort-qt Help

§2 Working with rooms

This section describes how to create, edit, and style rooms, and the commands on the Rooms menu.

Creating a room

Add Room on the Edit menu (Insert) places a new room at the center of the current view. The new room's number is reported in the status bar. Right-clicking an empty part of the canvas offers Add Room Here, which places the room under the pointer, along with Paste, Select All, Map Settings..., and Application Settings....

The Edit menu's Add Connected Room submenu instead adds a room already joined to the selected room in a chosen direction; see §3 (Working with connections).

Drag a room to move it, and select a room then drag one of the eight square handles on its outline to resize it. Positions and sizes snap to the grid unless Snap to Grid is turned off (see §5); in connect mode rooms cannot be moved.

Editing a room

Double-clicking a room opens its Room Properties dialog. The dialog has two tabs, General and Appearance.

The General tab holds the room's Name, Subtitle, Region, References room, Description, Objects, and Objects position, along with the Custom offset, Dark, Start room, and End room checkboxes. Checking Custom offset enables the Offset right and Offset down spinners, which shift the objects list from its Objects position anchor.

The Appearance tab controls the room's Shape, Border style, Fill color, and Second fill (with a Second fill location), the Hand-drawn edges and Make all corners equal checkboxes with the four Corner: top-left, top-right, bottom-left, and bottom-right spinners, and the colors used for its border, name, subtitle, and objects.

Two Rooms-menu commands edit the selected room without opening the dialog: Rename... (F2) changes the name, and Change Region... (Shift+F2) changes the region.

Right-clicking a room offers Edit... and Delete. Copy Color (Ctrl+Alt+C) copies the selected room's colors and Paste Color (Ctrl+Alt+V) applies them to other selected rooms. When a room references another (its References room field), Ctrl-clicking it selects and reveals the room it points to.

Darkness

Three commands set whether the selected rooms are dark: Toggle Darkness (K), Force Dark (Ctrl+K), and Force Lighted (Ctrl+Shift+K).

Start and end rooms

Set as Start Room (Ctrl+F5) and Set as End Room (Shift+F5) mark the selected room.

Room shape

The Shape submenu offers four shapes: Square (Ctrl+H), Rounded (Ctrl+R), Ellipse (Ctrl+E), and Octagonal (Ctrl+8).

A rounded room's four corner radii start at 15 and can be set anywhere from 1 to 30 in the Room Properties dialog; values outside that range in a map file are brought back into it when the map is opened.

Stacking order

When rooms overlap, Bring to Front and Send to Back on the Rooms menu move the selected rooms above or below all the others. The order is saved with the map.

Joining and swapping rooms

Join Rooms (J) draws a connection between exactly two selected rooms.

The Swap (two rooms) submenu exchanges one aspect of two selected rooms: Swap Objects (W), Swap Names (Ctrl+W), Swap Formats (Shift+W), and Swap Regions (Alt+W).

Deleting a room

Delete Selection (Del) on the Edit menu removes the selected rooms.

trizbort-qt-1.0.1/help/03-connections.html000066400000000000000000000131371526521612000203770ustar00rootroot00000000000000 Working with connections - Trizbort-qt Help

§3 Working with connections

This section describes how to draw and edit connections between rooms, and the commands on the Connections menu.

Drawing connections

The Edit menu's Connect Mode (C) turns connect mode on and off. While it is on, dragging from one room to another draws a connection between them, and you cannot move rooms. As you point at a room, the docking port nearest the cursor is highlighted to show where the connection will attach; a plain click draws nothing, so drag to draw.

The Edit menu's Add Connected Room submenu instead creates a new room already joined to the selected room. It offers the eight compass directions - North, South, East, West, North-East, North-West, South-East, and South-West - and Up, Down, In, and Out, which also label the new connection.

Editing a connection

Double-clicking a connection opens its Connection Properties dialog. Its fields are the connection's Flow, Style, Color, Name, Description, and its Start text, Mid text, and End text, together with a Quick label and the door options Has door, Lockable, Openable, Locked, and Open.

Select a connection to show its handles: a solid dot at each docked end and a hollow dot at each free waypoint. Drag an end onto another room to re-dock it there; it attaches at the docking port nearest to where you drop it, so you can choose the exact side of the room. Drag an end away from any room to detach it. Right-clicking a connection offers Edit..., Add Waypoint Here, and Delete.

Connections menu commands

Five commands act on the selected connection: Reverse (V), Rotate Source Port ([), Rotate Target Port (]), Insert Room on Connection, and Make Orthogonal. A sixth, Delete Room's Connections (Ctrl+D), acts on a single selected room and removes every connection attached to it.

Make Orthogonal reshapes the selected connection into right-angle segments by adding bend points between its ends, so the line runs horizontally and vertically instead of diagonally. It applies to a connection whose two ends dock to a north, south, east, or west port; a connection that uses a diagonal port is left unchanged.

The Line Style submenu sets the line to Solid or Dashed (T), and the Flow submenu sets it to Two-way or One-way (A).

The Label submenu labels the two ends of the connection at once: Up / Down (U), Down / Up (D), In / Out (I), and Out / In (O).

Plain (P) resets a connection to a plain, two-way, unlabeled solid line.

The New Connection Defaults submenu has two toggles, Dashed and One-way, that set the style and flow given to connections drawn from then on.

trizbort-qt-1.0.1/help/04-selecting.html000066400000000000000000000057451526521612000200410ustar00rootroot00000000000000 Selecting - Trizbort-qt Help

§4 Selecting

This section describes the selection commands, including the Select submenu of the Edit menu.

Selecting by hand

Click a room or connection to select it, replacing any previous selection. Hold Ctrl and click to add an item to the selection, or remove it again. Dragging a box across the empty canvas selects everything inside it; hold Shift or Ctrl while dragging the box to add the enclosed items to the current selection instead of replacing it. Select All (Ctrl+A) selects everything, and Select None (Esc) clears the selection. Pressing 0 selects the start room, and Enter opens the selected element's properties (see §13).

Find Room... (Ctrl+F) locates a room by name, then selects it and centers the view on it.

The Select submenu

The Select submenu lets you select rooms or connections that share a property. For rooms: Unconnected Rooms, Rooms With Objects, Rooms Without Objects, and Rooms in Selected Regions (Ctrl+Shift+A), which widens the selection to every room in the same region as any room already selected. For connections: All Connections, Dangling Connections, and Self-Looping Connections.

trizbort-qt-1.0.1/help/05-view.html000066400000000000000000000066601526521612000170340ustar00rootroot00000000000000 Navigating and the View menu - Trizbort-qt Help

§5 Navigating and the View menu

This section describes how to zoom and navigate the map, and the commands on the View menu.

Zooming

Zoom In and Zoom Out change the zoom in steps, while Zoom In 1% (Ctrl+Shift+=) and Zoom Out 1% (Ctrl+Shift+-) change it one percent at a time. The Zoom To submenu jumps to a fixed zoom: 50%, 100%, or 200%.

Reset Zoom (Ctrl+0) returns to 100%, and Fit to Window (Ctrl+Shift+F) zooms so the whole map is visible. The current zoom appears on the right of the status bar. Zoom is limited to a sensible range rather than continuing without bound.

The mouse wheel and trackpad

Turning the mouse wheel zooms in and out, centered on the pointer. Hold Ctrl while turning the wheel to zoom in finer steps.

On a trackpad, a two-finger scroll pans the map instead of zooming; hold Ctrl and scroll to zoom.

Panning

Besides the scrollbars, you can pan by dragging with the middle mouse button. The arrow keys also scroll the view when nothing is selected, described in §13 (Keyboard reference).

Other view options

Reset Origin (Home) recenters the view on the map's origin.

Two checkable options control what is drawn: Show Grid and Show Text (Ctrl+F4). Snap to Grid controls whether room positions snap to the grid when you drag, nudge, or resize a room. A final item, Mini Map, shows or hides the Mini Map dock described in §1 (Introduction and the main window).

trizbort-qt-1.0.1/help/06-settings.html000066400000000000000000000074451526521612000177250ustar00rootroot00000000000000 Map properties and settings - Trizbort-qt Help

§6 Map properties and settings

This section describes the Map Properties dialog and the tabs of the Map Settings dialog, both on the Edit menu.

Map properties

Map Properties... opens the Map Properties dialog, which records the map's Title, Author, Description, and History.

Map settings

Map Settings... opens the Map Settings dialog, which has six tabs: Colors, Grid & Lines, Rooms, Fonts, Advanced, and Regions.

  • The Colors tab sets the colors used throughout the map.
  • The Grid & Lines tab sets the Grid size, Show grid, Snap to grid, Show origin, Line width, Arrow size, and Text offset.
  • The Rooms tab sets defaults for new rooms, such as the Default room name and Default room shape.
  • The Fonts tab chooses the Room name font, Subtitle font, Object list font, and Connection text font.
  • The Advanced tab sets the Handle size, Snap-to-element distance, whether to Use document-specific margins (with a Horizontal margin and Vertical margin), and Wrap text at dashes.
  • The Regions tab manages the map's regions.

Restore Default Map Settings on the Tools menu returns these settings to their defaults.

Application settings

Application Settings... on the Tools menu edits per-user preferences that are stored on the computer rather than in the map.

trizbort-qt-1.0.1/help/07-automapping.html000066400000000000000000000133551526521612000204070ustar00rootroot00000000000000 Automapping from transcripts - Trizbort-qt Help

§7 Automapping from transcripts

This section describes how Trizbort-qt builds a map from a play-session transcript, following the player's movements. The commands are on the Automap menu.

Importing a whole transcript

Import Transcript (one-shot)... reads a complete transcript file and builds the map from it in a single pass. The same command is also on the File menu as Import Transcript.... The questions described under Questions the automapper asks can come up during a one-shot import too.

How moves are mapped

Each compass move places the new room next to the room the player came from. up, down, in, and out are drawn as connections leaving the room from its north-northwest, south-southwest, east-northeast, and west-northwest ports, labeled at both ends (up/down or in/out), so an export keeps them as up, down, inside, and outside exits rather than compass directions. Moving back along a one-way connection makes it two-way, and moving between two rooms that are already connected in a new direction re-docks the existing connection instead of adding a second one.

When Guess exits from room descriptions is on (the default), a room's description is scanned the first time the room appears, and a short dangling exit is drawn for each direction it mentions: the compass words and their variants (northward, northern, fore, aft, starboard, port), up, ascend, above, down, descend, below, and the phrases all directions and every direction. No stub is drawn on a side that already has a connection, and a stub disappears as soon as the player tries that direction, whether or not the move succeeds.

Questions the automapper asks

With Rooms with the same name are the same room off (the default), a room name that matches an existing room whose description differs opens the Disambiguate Rooms dialog. It shows what the transcript said, lists the candidate rooms with their descriptions, and offers This Room, A New Room, and Auto-Choose the Rest, which picks the first candidate for the rest of the session. The dialog never appears for a terse transcript with no descriptions; same-named rooms are then treated as one room.

When the player moves in a direction that already leads somewhere else, a second dialog asks whether to keep the existing room, keep the new one, or keep both. Keeping the existing room leaves the map unchanged, keeping the new room replaces the old one, and keeping both places the new room beside the old.

Live automapping

Start Live Automap... follows a transcript as it's written, adding rooms and connections as the player moves. You can end a session two ways: the Automap menu's Stop Automapping command, or the Stop Automapping button that appears in the status bar while a session is active. The status bar also shows the automapper's progress.

While a live session is running, two commands advance it when it pauses for input: Step (F11) takes a single step, and Run to Completion (Shift+F11) runs to the end.

Stopping a session records everything it mapped as a single undoable step. Starting a new map, opening another, or quitting while a session is running stops it first and treats the mapped rooms as unsaved changes, so you are asked whether to save them. Only complete lines of the transcript are read: a prompt the interpreter has written but not yet finished waits until its command arrives.

trizbort-qt-1.0.1/help/08-validation.html000066400000000000000000000034041526521612000202100ustar00rootroot00000000000000 Validation rules - Trizbort-qt Help

§8 Validation rules

This section describes the optional map-validation rules on the Validation menu. Each rule is a checkable option; while it is on, the map marks the rooms that break it.

The four rules are:

  • Rooms Must Have a Unique Name
  • Rooms Must Have a Description
  • Rooms Must Have a Subtitle
  • Rooms Must Not Have a Dangling Connection
trizbort-qt-1.0.1/help/09-statistics.html000066400000000000000000000042661526521612000202600ustar00rootroot00000000000000 Map statistics - Trizbort-qt Help

§9 Map statistics

This section describes the map statistics report.

Map Statistics... on the Tools menu opens a Map Statistics report. It counts the map's rooms and connections and related figures, including:

  • # of Rooms, # of Dark Rooms, and # of Unconnected Rooms;
  • the Start room;
  • # of Connections, broken down by label, direction, and line style;
  • Total # Rooms with Objects, Total # of Rooms with subtitles, and Total # of Rooms with descriptions.

The same report can be printed without opening the editor, with the --stats command-line option; see §12 (Command-line usage).

trizbort-qt-1.0.1/help/10-exporting.html000066400000000000000000000056601526521612000200740ustar00rootroot00000000000000 Exporting - Trizbort-qt Help

§10 Exporting

This section describes how to export a map as an image, a PDF, or interactive-fiction source code using the File menu's Export submenu.

Image and PDF

PDF... writes the map to a PDF file, and Image (PNG/JPEG/BMP)... writes it to an image.

Source code

The Export submenu also writes the map as source code for six interactive-fiction systems: ZIL (Infocom / ZILF), Inform 7, Inform 6, Alan, Quest 5, and Quest 5 (rooms only).

For Inform 7 you can name either a single .ni source file or an Inform project folder ending in .inform; in the second case the source is written to Source/Story.ni inside that folder, ready to open in the Inform IDE.

The generated source uses the map's title, or the map's file name if it has no title. A map that has never been saved is exported as "A Trizbort Map". Rooms with no region are written as regionless in every format, and quotation marks and other characters that are special in the target language are escaped in names and descriptions.

The File menu's Export to Clipboard submenu offers the same six formats, but copies the generated source to the clipboard instead of writing a file.

You can also produce every code format from the command line without opening the editor; see §12 (Command-line usage).

trizbort-qt-1.0.1/help/11-files.html000066400000000000000000000056621526521612000171620ustar00rootroot00000000000000 Files and formats - Trizbort-qt Help

§11 Files and formats

This section describes how to open, save, and back up maps, and the .trizbort file format, using the File menu.

Opening and saving

New (Ctrl+N) starts an empty map, Open... (Ctrl+O) opens one from a file, and Open from URL... (Ctrl+Shift+O) opens one from a web address. Save (Ctrl+S) and Save As... write the map back to disk. Recent Maps lists maps opened recently. A download started with Open from URL... can be canceled from its progress dialog and gives up on its own after 30 seconds without a response.

Maps are stored in the .trizbort format, the format used by the original Trizbort. Files written by older versions open normally: missing or duplicate element numbers are reassigned, rooms with no region are placed in the default region, and a saved map is always stamped with the current format version so that it opens with the current color rules in the original Trizbort too.

Backup and Smart Save

Backup (Ctrl+B) writes a backup copy of the map. Smart Save (Ctrl+Alt+S) saves the map and also writes a PDF and/or image alongside it, according to the Smart Save preferences in §6 (Map properties and settings).

trizbort-qt-1.0.1/help/12-command-line.html000066400000000000000000000071661526521612000204250ustar00rootroot00000000000000 Command-line usage - Trizbort-qt Help

§12 Command-line usage

This section describes the command-line options. With no options (or with a map file), Trizbort-qt opens the editor; the options below control start-up or run one job without opening a window.

Start-up options

  • --loadlastproject (-a) opens the most recent map on start-up.
  • --exit (-x) exits after any batch task instead of opening the editor.
  • --version (-v) prints the version and exits.

Batch modes

Each of these runs one job and exits without opening a window:

Option Effect
--save load and re-save the map to a new file
--quicksave (-q) re-save the map in place
--smartsave (-s) re-save, plus a PDF and/or image per the Smart Save preferences
--render render the map to an image
--pdf render the map to a PDF
--stats print the map statistics report (see §9 (Map statistics))
--import-transcript build a map from a play-session transcript

Exporting from the command line

Six options each export the map as source code (see §10 (Exporting)): --zil, --inform7, --inform6, --alan, --quest, and --questrooms. When the output name given to --inform7 ends in .inform, the source is written as Source/Story.ni inside that project folder.

trizbort-qt-1.0.1/help/13-keyboard.html000066400000000000000000000233651526521612000176620ustar00rootroot00000000000000 Keyboard reference - Trizbort-qt Help

§13 Keyboard reference

This section lists the keyboard shortcuts, grouped by the menu they belong to, followed by the keys that move around the canvas.

File

Command Shortcut
New Ctrl+N
Open... Ctrl+O
Open from URL... Ctrl+Shift+O
Save Ctrl+S
Save As... Ctrl+Shift+S
Backup Ctrl+B
Smart Save Ctrl+Alt+S
Quit Ctrl+Q

Edit

Command Shortcut
Undo Ctrl+Z
Redo Ctrl+Y
Add Room Insert
Delete Selection Del
Connect Mode C
Cut Ctrl+X
Copy Ctrl+C
Paste Ctrl+V
Copy Color Ctrl+Alt+C
Paste Color Ctrl+Alt+V
Select All Ctrl+A
Select None Esc
Find Room... Ctrl+F
Rooms in Selected Regions Ctrl+Shift+A

Rooms

Command Shortcut
Rename... F2
Change Region... Shift+F2
Toggle Darkness K
Force Dark Ctrl+K
Force Lighted Ctrl+Shift+K
Set as Start Room Ctrl+F5
Set as End Room Shift+F5
Square Ctrl+H
Rounded Ctrl+R
Ellipse Ctrl+E
Octagonal Ctrl+8
Join Rooms J
Swap Objects W
Swap Names Ctrl+W
Swap Formats Shift+W
Swap Regions Alt+W

Connections

Command Shortcut
Reverse V
Rotate Source Port [
Rotate Target Port ]
Delete Room's Connections Ctrl+D
Dashed T
One-way A
Up / Down (label) U
Down / Up (label) D
In / Out (label) I
Out / In (label) O
Plain P

Automap

Command Shortcut
Step F11
Run to Completion Shift+F11

View

Command Shortcut
Zoom In Ctrl++
Zoom Out Ctrl+-
Zoom In 1% Ctrl+Shift+=
Zoom Out 1% Ctrl+Shift+-
Reset Zoom Ctrl+0
Fit to Window Ctrl+Shift+F
Reset Origin Home
Show Text Ctrl+F4

Help

Command Shortcut
Help Contents F1

Canvas navigation

The arrow keys and numeric keypad move around the map. With a room selected, an arrow key nudges the selection; holding Ctrl navigates to the room in that direction, creating one if none exists; holding Ctrl and Alt resizes the room; and holding Shift follows a connection. With nothing selected, the arrow keys scroll the view. The numeric keypad navigates in all eight directions.

Two more keys act on the whole map: Enter opens the properties of the selected room or connection, or - when nothing is selected - selects the room nearest the middle of the view; and 0 selects the start room. The mouse wheel zooms (hold Ctrl for finer steps), described in §5.

trizbort-qt-1.0.1/help/help.css000066400000000000000000000034251526521612000164100ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * Applied as the QTextBrowser default style sheet by HelpWindow. Keep to the * CSS2 subset that Qt rich text understands (no flexbox, grid, or variables). */ body { font-family: sans-serif; font-size: 10pt; color: #202020; background-color: #ffffff; } h1 { font-size: 16pt; color: #1a3d6d; } h2 { font-size: 13pt; color: #1a3d6d; } h3 { font-size: 11pt; color: #333333; } a { color: #1a5fb4; text-decoration: none; } p.stub { color: #808080; } nav.crumb { font-size: 9pt; } nav.pager { font-size: 9pt; color: #808080; } code { font-family: monospace; color: #6c1f8f; } code.ui, code.sym { color: #0a5d2a; } code.key { color: #7a3b00; } kbd { font-family: monospace; color: #7a3b00; background-color: #f0f0f0; } table { border-width: 1px; border-style: solid; border-color: #cccccc; } th { background-color: #eef2f7; text-align: left; } trizbort-qt-1.0.1/help/help.qrc000066400000000000000000000025571526521612000164120ustar00rootroot00000000000000 index.html help.css 01-introduction.html 02-rooms.html 03-connections.html 04-selecting.html 05-view.html 06-settings.html 07-automapping.html 08-validation.html 09-statistics.html 10-exporting.html 11-files.html 12-command-line.html 13-keyboard.html trizbort-qt-1.0.1/help/index.html000066400000000000000000000067531526521612000167520ustar00rootroot00000000000000 Trizbort-qt Help

Trizbort-qt Help

Trizbort-qt is a visual mapping tool for interactive fiction.

Contents

  1. §1 Introduction and the main window - This section describes what Trizbort-qt is and the layout of its main window: the map canvas, the menu bar, the toolbar, and the docks.
  2. §2 Working with rooms - This section describes how rooms are created, selected, edited, and styled, and the commands on the Rooms menu.
  3. §3 Working with connections - This section describes how connections between rooms are drawn and edited, and the commands on the Connections menu.
  4. §4 Selecting - This section describes the selection commands, including the Select submenu of the Edit menu.
  5. §5 Navigating and the View menu - This section describes how the map is zoomed and navigated, and the commands on the View menu.
  6. §6 Map properties and settings - This section describes the Map Properties dialog and the tabs of the Settings dialog.
  7. §7 Automapping from transcripts - This section describes how Trizbort-qt builds a map from a play-session transcript, both live and in one shot.
  8. §8 Validation rules - This section describes the optional map-validation rules on the Validation menu.
  9. §9 Map statistics - This section describes the map statistics report.
  10. §10 Exporting - This section describes how a map is exported to PDF, to an image, and to interactive-fiction source code.
  11. §11 Files and formats - This section describes opening, saving, and backing up maps, and the .trizbort file format.
  12. §12 Command-line usage - This section describes the command-line options, including the headless batch modes.
  13. §13 Keyboard reference - This section lists the keyboard shortcuts.
trizbort-qt-1.0.1/packaging/000077500000000000000000000000001526521612000157365ustar00rootroot00000000000000trizbort-qt-1.0.1/packaging/app_icon.qrc000066400000000000000000000012031526521612000202310ustar00rootroot00000000000000 trizbort-qt.svg trizbort-qt-1.0.1/packaging/generate-icons.sh000077500000000000000000000100521526521612000211760ustar00rootroot00000000000000#!/bin/sh # SPDX-FileCopyrightText: 2026 Jason Self # SPDX-License-Identifier: GPL-3.0-or-later # # Regenerate the platform icon files that are committed beside their vector # source, packaging/trizbort-qt.svg: # # packaging/trizbort-qt.ico -- baked into the Windows .exe # packaging/trizbort-qt.icns -- the macOS .app bundle icon # # These two are the only rasterised icons the project ships. The running # program's window/taskbar/dock icon is rendered straight from the embedded SVG # at run time (see packaging/app_icon.qrc and main.cpp), so no PNG ladder is # committed. The .ico/.icns must exist as real image files because the OS reads # them before the program starts; committing them keeps a from-source build on # any platform free of image tooling. Run this only when the artwork changes. # # It needs an SVG rasteriser and icon packers, chosen from whatever is # installed: # SVG -> PNG : rsvg-convert (librsvg2-bin) | inkscape | magick/convert # PNG -> .ico: icotool (icoutils) | magick/convert # PNG -> .icns: png2icns (icnsutils) | iconutil (macOS) | magick # # On Debian/Ubuntu: sudo apt-get install librsvg2-bin icoutils icnsutils set -eu here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) svg="$here/trizbort-qt.svg" ico="$here/trizbort-qt.ico" icns="$here/trizbort-qt.icns" [ -f "$svg" ] || { echo "error: $svg not found" >&2; exit 2; } have() { command -v "$1" >/dev/null 2>&1; } tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT # --- 1. Rasterise the SVG to square PNGs at the sizes the packers want. ------- sizes="16 24 32 48 64 128 256 512 1024" rasterise() { # rasterise _s=$1; _o=$2 if have rsvg-convert; then rsvg-convert -w "$_s" -h "$_s" -o "$_o" "$svg" elif have inkscape; then inkscape "$svg" -w "$_s" -h "$_s" -o "$_o" >/dev/null 2>&1 elif have magick; then magick -background none -density 384 "$svg" -resize "${_s}x${_s}" "$_o" elif have convert; then convert -background none -density 384 "$svg" -resize "${_s}x${_s}" "$_o" else echo "error: no SVG rasteriser found (need rsvg-convert, inkscape or ImageMagick)" >&2 exit 3 fi } pngs="" for s in $sizes; do out="$tmp/icon-$s.png" rasterise "$s" "$out" pngs="$pngs $out" done echo "rasterised: $sizes px" # --- 2. Assemble the Windows .ico (16/24/32/48/64/128/256). ------------------- ico_pngs="$tmp/icon-16.png $tmp/icon-24.png $tmp/icon-32.png $tmp/icon-48.png \ $tmp/icon-64.png $tmp/icon-128.png $tmp/icon-256.png" if have icotool; then # shellcheck disable=SC2086 icotool -c -o "$ico" $ico_pngs elif have magick; then # shellcheck disable=SC2086 magick $ico_pngs "$ico" elif have convert; then # shellcheck disable=SC2086 convert $ico_pngs "$ico" else echo "error: no .ico packer found (need icotool or ImageMagick)" >&2 exit 3 fi echo "wrote $ico" # --- 3. Assemble the macOS .icns. --------------------------------------------- if have png2icns; then # png2icns keeps the resolutions it recognises and ignores the rest. # shellcheck disable=SC2086 png2icns "$icns" $pngs elif have iconutil; then # iconutil (macOS) needs a specially named .iconset directory. set="$tmp/trizbort.iconset" mkdir -p "$set" cp "$tmp/icon-16.png" "$set/icon_16x16.png" cp "$tmp/icon-32.png" "$set/icon_16x16@2x.png" cp "$tmp/icon-32.png" "$set/icon_32x32.png" cp "$tmp/icon-64.png" "$set/icon_32x32@2x.png" cp "$tmp/icon-128.png" "$set/icon_128x128.png" cp "$tmp/icon-256.png" "$set/icon_128x128@2x.png" cp "$tmp/icon-256.png" "$set/icon_256x256.png" cp "$tmp/icon-512.png" "$set/icon_256x256@2x.png" cp "$tmp/icon-512.png" "$set/icon_512x512.png" cp "$tmp/icon-1024.png" "$set/icon_512x512@2x.png" iconutil -c icns "$set" -o "$icns" elif have magick; then # shellcheck disable=SC2086 magick $pngs "$icns" else echo "error: no .icns packer found (need png2icns, iconutil or ImageMagick)" >&2 exit 3 fi echo "wrote $icns" echo "Done. Review the diff, then commit $ico and $icns." trizbort-qt-1.0.1/packaging/org.jxself.trizbort-qt.metainfo.xml000066400000000000000000000035671526521612000246550ustar00rootroot00000000000000 org.jxself.trizbort-qt FSFAP GPL-3.0-or-later Trizbort-qt Map maker for interactive fiction and text adventures

Trizbort-qt is a cross-platform mapper for interactive fiction. Draw rooms and the connections between them, annotate them with descriptions and objects, and export the map as source code for many interactive-fiction systems (ZIL, Inform 6, Inform 7, Alan and Quest) or as a PDF or image.

It can also build a map automatically from a play-session transcript, following the player's movements. It reads and writes the .trizbort file format used by the original Trizbort.

trizbort-qt.desktop trizbort-qt application/x-trizbort Graphics

Initial packaged release of the Qt/C++ port.

https://jxself.org/trizbort-qt.shtml Jason Self
trizbort-qt-1.0.1/packaging/trizbort-qt.1000066400000000000000000000057651526521612000203360ustar00rootroot00000000000000.\" SPDX-FileCopyrightText: 2026 Jason Self .\" SPDX-License-Identifier: GPL-3.0-or-later .\" .\" Copyright (c) 2026 Jason Self .\" .\" This file is part of Trizbort-qt. .\" .\" Trizbort-qt 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 3 of the License, or (at your .\" option) any later version. .\" .\" Trizbort-qt 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 Trizbort-qt. If not, see . .TH TRIZBORT\-QT 1 "2026" "trizbort\-qt" "User Commands" .SH NAME trizbort\-qt \- map maker for interactive fiction and text adventures .SH SYNOPSIS .B trizbort\-qt .RI [ map.trizbort ] .br .B trizbort\-qt .I map.trizbort .B \-\-save .I out.trizbort .br .B trizbort\-qt .I map.trizbort .BR \-\-zil | \-\-inform6 | \-\-inform7 | \-\-alan | \-\-quest .I out .br .B trizbort\-qt .I map.trizbort .BR \-\-render " " \fIout.png\fR " | " \-\-pdf " " \fIout.pdf\fR " | " \-\-stats .br .B trizbort\-qt \-\-import\-transcript .I play.txt out.trizbort .SH DESCRIPTION .B trizbort\-qt is a cross\-platform reimplementation of Trizbort, a mapper for interactive fiction. It edits the \fB.trizbort\fR map format: rooms, the connections between them, regions, colors and per\-map settings. With no arguments, or with a map file, it opens the graphical editor. The command\-line options below run a task without opening a window and then exit. .SH OPTIONS .TP .BI \-\-save " file" Load the map and write it back out (round\-trip / format upgrade). .TP .BI \-\-quicksave ", " \-q Re\-save the map in place to its own file. .TP .BI \-\-smartsave ", " \-s Re\-save the map and write a PDF and/or image beside it, per the Smart Save preferences. .TP .BR \-\-zil ", " \-\-inform6 ", " \-\-inform7 ", " \-\-alan ", " \-\-quest ", " \-\-questrooms " " \fIfile\fR Export the map as source code for the named interactive\-fiction system. .TP .BI \-\-render " file" Render the map to a raster image (PNG, JPEG or BMP, by extension). .TP .BI \-\-pdf " file" Render the map to a PDF document. .TP .B \-\-stats Print map statistics to standard output. .TP .BI \-\-import\-transcript " in out" Build a map from a play\-session transcript and write it to \fIout\fR. .TP .BR \-\-loadlastproject ", " \-a Open the most recently used map on start\-up. .TP .BR \-\-exit ", " \-x After any batch task, do not open the editor window. .SH FILES .TP .I ~/.config/Trizbort/Trizbort-qt.conf Per\-user preferences (recent files, tooltips, export options). .SH SEE ALSO Project home page: .UR https://jxself.org/trizbort-qt.shtml .UE .SH AUTHOR Qt/C++ port by Jason Self. Based on the original C# Trizbort by Genstein and Jason Lautzenheiser. trizbort-qt-1.0.1/packaging/trizbort-qt.desktop000066400000000000000000000023131526521612000216310ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2026 Jason Self # SPDX-License-Identifier: GPL-3.0-or-later # # Copyright (c) 2026 Jason Self # # This file is part of Trizbort-qt. # # Trizbort-qt 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 3 of the License, or (at your # option) any later version. # # Trizbort-qt 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 Trizbort-qt. If not, see . [Desktop Entry] Type=Application Version=1.0 Name=Trizbort-qt GenericName=Interactive Fiction Mapper Comment=Draw maps for interactive fiction and text adventures Exec=trizbort-qt %f TryExec=trizbort-qt Icon=trizbort-qt Terminal=false Categories=Graphics; MimeType=application/x-trizbort; Keywords=interactive fiction;text adventure;map;mapper;IF;adventure; StartupNotify=true StartupWMClass=trizbort-qt trizbort-qt-1.0.1/packaging/trizbort-qt.icns000066400000000000000000001343171526521612000211260ustar00rootroot00000000000000icns¸Ïicp4Þ‰PNG  IHDRóÿa�IDATxœ�“ÍoLaÆï{ïLç^¥ÓjUÔG¥E‰ŠL£>"%±±e!aÇ_a)6HÄ? ¬Jʱ�H‘Eˆª¯fLÕT;ýH§íLÇÌÜ{ßc15¦l<ÉÙ�œœçü΃ˆÄ<Ïö<Ïxžo|?cŒTÊ÷ñ–ªèù&ĈȰˆÄ”ˆ$€Í€ŠÿÓïÞ%"ÒÛûA>}ü®"NˆB!àØ±=45Õ""ˆnÝ`&�Ak…sòÄ7,€Ò~€<ëP‡Åé꣱î ÏŸÇÑZaYšÌl‘žÁ§Œï&µ/νäS_fT±•BÕ×»lk_8H.\bjj¥aæÇ"µõ[ÚÅÜ`Pö¡”R6@:�c*Y¤zµ0::KggÇ�ï`2™áÜíOL§²  51�Ù.å!¶¥¡mûNnt�0‘šÇ¶¢œ9ÛF4ê°²ÚáàÚݼºð•Õõ+è¨kgcKÝÒ@%®îîWòîÝØºJ?DD¤çæk|›”¿¥+Ù¸®E(¬þLÿ ¨r5¡H©Ý˜ ‰¯S$â)^¾ˆ39žf}S‘HcˆðæýwžôÇK“^ÈÓÙÑ\~ ÂÕký\ÿ¸@nÇVî&²ôõ  Yض¦`„žK÷9j´¥fxpù>Ÿã“åMlw•KóÑчH´š‡Èdòh­�˜gC­Ë‘ý- `.ï“/•D~æ=µø³HUÄ&›ÍÓØ°’ÖÖ5(–&9ô rК\®€1¦t±A«u5U2ÔóXEk\¦Çg9uz/±X3»dç q¥omk&‹BWã*±,­ÊaÊÍ/ŠÒZÙ¶&äT•/­µÂ!›óJ¤"6aK—ĈĂR4ÍR-‹³ù‡¼,‹ó/ìc®X³D&GIEND®B`‚icp5‰PNG  IHDR szzôÀIDATxœÅ—[ˆ]ÕÇkí}®s.™3Ç™É(:&K;ä!`H1c½€‚¢i@$!‚­ÐÓÒRZ¼…–D©­/VlƒF}òÍhÔ(”$ ƒHTâ5ÔÐJ¬:ÓÉœû9{¯õõaŸ½çì™}F_¬lÎÞûìÿ÷ý×w[ß@DR"²_Dþ-"F¾;1}ûE$EßxFDŽ|‡F‡ÉÉ(Ùüð¬ÅUJ ¢Tâ똈È�p| P"ò%P5Æ*ÇÑC­ˆÈ·"1ÏJÌ+ Ýhti·=ôH¥Êåì7Y˜o c$ÖZ*Õ‘¡ä]c,Ž£9vìCöÞó ùw™# ´bi©Ã½÷þ”ÛoßFøý2AÁËÏï>Ä›§>f¤˜Á𬵢ÓñØtÙ$Ï>s7…Bˆ{Ò pôèG\sµæ¾7ã·:¸®ÂÁÉ¥yêïŸrøðûìܹß·ØZAkÅâB‹7N}Ì­�OSº(ƒß³(¸\Dqh×>ÿ×"?œ]�µ±Pà†7étŠ|:ÃØXJ€«À¸*• ££GÇVï8ÁïEãEJ¥,ÅjšòD6FÀ¡PʬMÁ3‚ˆÅïY\Û÷€cp—wÞ9ÇÁƒÇ1F¢ ³¾Ô¡^kð=Y&`‰B²& Å/Áu5 Nž<‹1Ë1 tÛþr•$èXK"J©H±RDŠ@Óju¹á†Y{ìŽd-[çîÃX‹Ö ƒÄIh5”HD Ûõp•·i” H�l‹‹-Œ±±*Wýßù­V/†¿J "ŠnÇà ƒ®zûöi~óë¼qò-ÄJdÀq¾8ß`ï/·E†W–ay4ÏÌô$‡v}@¡˜Áø¥Z)º]�ñü(“S¥•Í(ðö`#:wn�z½‹Öc,{ö<ÁÜÆìì33ãÉÞï+m·{|vv¥`÷î'Ø·o›6Mày†K§+”ʹD±*˜ž‹ýY©dÙ¼yŠ�Çû±:�¡Â\.Í�6OP®dùÁìz6m\&�d~ü#ŽþãܔÙ³_±ï¡—˜˜(Ñn{ÌmÛÀ-7oIœ)”çùâº�þùüíu®¼q �f¥Åbƒâô‰3ìüÉùË.|ßF +¸ÞèrõUûØ>Y RÌ‘ÉÐlu±Vð}Ëá÷Ïóê‰ßqùeÕh]‚Úb“­7na÷w²ôµ‡v4^ÏgÝx†òãëh~úO ž¨áj<ÏPÈ¥xäÎíäÖå¡ëƒ£ï[ÅÛ¡Ñì%‡ Š¡«i7[,}íQ›¯¡�5AooÕÚäû+,ÉÐ¥Ö üÕ…6(zþÀvlÁïëY“@� íèè‚`0*­ËóòSïrÝõ°Æ®Ú ;]ŸF­C&íà(pû›�R`Í&Zc±&€YcQÍz‡¹¹+øýý;7£ KmöþâizžÁŸ¾!7’¥TM!RŒBPª¦IçÓ”+fg/NTÒlõH§ªÅÙrެoƒ–¡ )WÙÝð}*“â½×ßeô¯ÇhÖZQ÷Ë—r¼ýÚi®»¢Ò�õjôz>‹Km}á4c¥ž±�}LUŸY'“v (k­(¥øâ?øÓC/ѨµpäD߈vîùÕMÌÌL¬*£�ÄsϽŋGß#›Mcû«Ò*˜Šç~¼‘ŸÝum?,+ú@x0aÙiÿ‰&xÐÖZßóŒ„SÏàôôµ7£ð»$¬1±L �f�mùž§¥ïñxþ?‚ ämIEND®B`‚icp6¸‰PNG  IHDR@@ªiqÞwIDATxœí›Ëoׯgf(¾BR¤PD @¼ê.‹fÓè¢@‹¢mœ(+g㮲h²hÑdĆ�ä/HPYuã6€…®· ºÐº±ÑE”ÆIÛ¤¶’6V,�Ôƒ²Ä™{º˜Š‡Ô̈¤`[@Pºœ{¿{Î=÷ùͨª-"^ð÷O€ ÀS@°x0a€UàoÀïDä:ôÛJ˜|ŸQÕ÷õáÅûªz¦×fÑÀªúà�@ÐÀ{ Sk¯Éà - à9YVU[TU€ï*€ 8ÇTÙI#´­< üÇÞáá7|Û\|[ßUýðöÃäQ@hë�-àÅ Q�¯>SGhë‹¢ª«ÀB�ØðT{Ÿ‹ AÆ0dN�;´õ+QUÃ�‘ÞŲÒYbŒÁ²Ò÷¤)s«¨jŸ«UAÄ¯ÈÆF;(ð°ÖŒ1‹92»[FRôr·Bn=„[|îÇRr÷�ø¡÷——oñê«ïÑlî "1ÌU%—s¸té‹‹ßKÜ’¹[Í,K·ßGÙœÃÅKÏ$æî‹Up]—ï?ý&Ÿ¬¬R¯—1ÆÄ*Ȳ,ÖÖÚT*EnÞ¼L­öªŠÄlUèt\Ξ}“••UæJoZßî0;[äÆ�Ëœ:Ÿ»a†v{—F£M½^â¯~’rÕA;zhX)ð³g?æ“O·h4¶Ä Ç^îf£ÍÜB‰ ø.ùY/7ÀÕ Ÿ±öy›Fc;p@<î�E�ˆ`Y‚1J­êP˜uÀÓ˜ bÿ9ÛN7ˆu¹=%_qÈWã;@ñŸK:€F®úÂNÑé(êi¬è© 1{M$oÈm\Åëhl„ù“bä²W$#I6²&m…°ì¾|²Ï?޵Å0Œ}髪¸®wøƒ}yüo×õb|ãÂø7> çÏ_!—›I0úÏíu\67v(ÔœTáœcw€ˆpûö=\7¹¶-Øb�»J#1þXZz™……S±$á4¸¹¹Ãó‹o³çíM´ß÷b"{ÿ'ž˜c~¾–8ßînÛ¶ÐdCÈ‘0ììt0FG@»½7µ¾"Ò"éæÔ–%]Ãã9`4­Ð10 ª*Æ$ŸË�îÚ;íTvÜ�ËÍPÈ;¬®6ùé³3t+,ý?‰À?ÿµE.kS*å»iI�ÏÏ�Ë;ܽÓä÷>CÑàäBº|QK„µ/·q²6år2î®Düõ6›áõןá•WÞãŸn²Oœ£¨ñÿ ½’e³6¯½ösææÊɶ¤Üw?oû[qU¿;ª"å…upfRr;i4¶Y_ßĶm¦s]à /\áöíu®]{‰z½†ë*•sså#ˆ4ÛÜ[ßĶ-Pp½}¾¥k/±°Pèb‰ÆSÊå|*îˆÝ ß�ªÕ"ÕjqhÆB!‹ëΜ9Íü|µ›~”#­QÜÅB/‚¯i¸#gÿ$FGÎá`Ó;åÙ¶¤6þ0î(¾^®4[a±‰7%õNyqOÃ(î¤SìaxT„�¡8qÀqWà¸qâ€ã®Àqã‘wÀÐi0®@éÏÙágòâh?_ï/é¸#°¿È^bw îØˆHW Ÿ8ÚÏ=Œ¯?orî¨ÒFZ­,[Ã3†Ý=ËšÍ6•Ùžk(•Ž.ކÜ­v×Ï3ìív°,¡Õl³Q)ì;*X>‹ÙTÜ}›¡A�²}@ ô·ƒªÊÆÆ¥RÎ?ÆR%›ËpñâxÄÑf«�ê°1ÊÆæ}P(•sØ–ì¿ÌìsY‡K)¸ÄQ_ |ƒ••¯™›¯F08¶zëyUŸ°yo“ÙÙBb�2äv;.OŸ}ƒ••¯¨Ÿ®ìs 8¶ÿ’—ëy)–ðmc›Êl‘›7.S;š8ºç ”óU~ñÛ_’/ð\o °ÐoÝt…«¿~—»_|C£ÑN$Pör7šmê§+üé7?¤Z˜¡ãiÏÝ£ß^æÖ×M_˜‹8Ú1äË…¡ˆ6$ý®¬�Û(Õ µÄ!{œâ¨q=¼à“$”Ó¨öæíxÚý$G“R�> ¶¦’h«;q´W=G'ˆñ # œ?…|>“Xít\66ïsª˜EÓö£„˜˜8êyÉß’°mAìYUaiéeêõZbilkó>Ï=ÿ^ÇÛñÚa˜˜8úøãÑ'·£°·ëbÛ‚Ù›@¥†àD�JÃÝ;ë\ýÕ»²ÜÈ׿ÒkwÖpfÊå\P©Du!—Ï�ÏeX]k±øÖ2¢QRè ,þý¿ÙŒMiœâèÝ/¾9(G"ä²gœ±‰£·þÛ¤G•P²Nzq´ïuù~�r«+PŽ.ŒgR ”QÜë÷îäžIÍ­‘&ŽöÎ~ú¼Säî^˜p€�ð`≣Q8ÊV8Ä”¸C[?:¹4%" î±Vk:pñmýPD>8¹8 X"ò%pÿZ©ƒ?Hx<Wézmqðm<ØlYÁ½a[D–ño‹_Ç!mü{ÃÐoËuà©î½a¯gþ4¯ÏÿZ Nà»Y×IEND®B`‚ic07 *‰PNG  IHDR€€Ã>aË éIDATxœí�[ŒÕÇß™±×»Ë:8 .J¸¼q‰–ËK_"E B…à�TÞPûPÄEê HU‘xA­"¡ª­ò¤>”¨‰úB+ �@H…& E(‰B"B²‹¯½öÌׇ™ñz{×v<3Ïù­Ž,¯=sæ›ÿfΜ3ÇŸ° ªêˆˆ×ñþvàaààNà`Ëfë±ÄÂ2pøøxWDŽFvk× é÷�ª `¢¨ê“ÀóÀ.`þÊ·Ý—€��}"r¾ˆh¯z@U  "¢ªú0ðàg_ñ»–ïk$K¬h׫éøì3à÷"ònx0‹ˆøtq™pªjDÄWUxx)ü(:•˜^ËY&eíàtÂ×½ÀïD¤iÛ¹À:!;Ä¿ø;ðkÂ;X²D§nïO‹È¹n´ �ö� ðpÐ ‰m²%" �÷è¸h7ø„À-ï`ÅŸ& ZÞE ­H¨y»ÑµößvcÅŸ6"ìÞ µ68ÁOUNp­hnZ[j‰•HÛ‡Dä}Uu¤ãÚÿpAãÁ6ø¦“HÛ/�û‰Z„�cÅÏ�Æ÷�‹ˆµ^`­3Á2ý(�截î¥ðÛÉ3ÝD×�Eì!ßÊŸ„@ë°ÇtXòÉý†`HìÑŸ'"­ïUý XÀ^ÿóD¤uUTuèÖÿ‹Œ„ÈdùqãªÇÏ÷UÅqÌæ_¾ìpcÒ5Â4Ç=ð@UÛάÕ¬®zÄeTUpÃÂB vHZ&˜ö¸:D;áĉyýõòùçßÒlúˆ=N¢u:ްsçv^}õ—ìܹ=ä!îMϾ¯ˆÀ‰çÙ³çO|óÍYÊ幨ނN.^¬S©ÌsèÐKÜ{ï-‰š`£¸ãh-GëTàR‚qozQS %o¼q�ãÇÏrã� '\4¾â8†ë®+³´Tãµ×þ‘X,¢_ÜF #†qþ9ák!á¸7¼ aee•O?ý–-[fY]mJ¥âÆz¨V=�åò,_}uŠÓ§—ض­’ÈY`£¸K[œXãn\ô�{ 6@£Ñ¤ÕòZ­@üÿk‘òÖÚÔXvÈcOæ¿ÇjÌ– žç³²²:þJ6¡3n¿¥”®vøÕþ;˜­¸x1ÅýösGùá›:nI‰{ tß—ŠÀÖ­æ+ð4–î#Çé®3ù»€^qÏ^í2·5>HÂq�üäO³©¨§±�¾äŒßR¼¦Æf€¤åG6€HPˆ^ó‚¬Å> q'Óµe™X¬rŽ5@ÎÉŒÒhNjCtœdÆÑma ¯¨Žî[Ñi$PUêõ&þeœÇOTG£ÑjÍN+“=H׎ÆgŸýW]5“hõõz“ju×5xÍé4Âd DD8uê<ž—Àáß�1B±˜‰]42™‰.-!’…LšÌ`Ú…H‹É6@Ç“7û÷ÿš›o¾&±á`¡Z]áé§ÿÂòr ‘L´—‡f² ÐÁŽ׳}ûÖDël4š8Ž I´æäÈŒVVšø¾&z¨ÕV§¾3(30FÚÂÇo€àÎ#íÇÑ“`:/l–�±È9Ö9Ç çXäœ� 0 ½pilCâè6°PpÛ·DŽ#T«�=qãHlO±ž8Yg¦t¤5(Ó·q„ÆE�·Ÿ;<ºSÜ˧8Å`ýIĽáÚEßWæçgX\¼‰ƒ¿àúëË4-Ž«�$ÃÞ*÷Úq=Ö1S4‹ËË5î¾û¶m« šÌÜÀ�â>÷¿zxTöÚŽ~®|›Ý¢$÷€“C…#GNñè£di©F¹<;Ò“9Á2ÑÈ€~µ«Ó±A8pà|ð|ßǘäæè÷‹{C™e}|èà' !ù¸7]«1‚ª²sçvz‰]»nÃuM[8ÕÁ‹ç�çùø>íÒý�h�Æ‹‹7óÎ;¿ w‚&&þfq ªvßßóQŸµÒã{½J$~Òqü�}ð'Ož§ÑhUÅun½õÚËêNšAâ6¾~¤÷À- c¤=gþ¦›F•+—gñ¼êuÓQ¾$Oû½6îaãëGRqÕÄŒÜx%Jv/»Ñ(_0”~WÅ0qßFõ%÷H÷ã<-%9Êw¥Œ²}“_ú‡—%U¬rŽ5@αÈ9Ö9Ç çXäk€œc �s¬rŽ5@αÈ9Ö9Ç çXäk€œc �s¬rŽ5@Îé™ÀqÎWSÕŽ²þ³iȺQ|ý˜êÌ¡ÝFº®ƒˆôü]Þ,f&¾�êëµ®8ØkóÓdè šþäk³éqéÒêºÀ––j,/׺&†LVæÐÍâ6¾þu%÷@3ƒÆ•AÓ÷}ªÕúºÿ-,”Ö=ŸåÌ¡ƒÄ×�Üdu]gÝûVË[÷>­ šÝŒ·ë®ÛkùCÏ$O2cꈙC F# ]¼–·®tIgÐ,ngÓ›ž®+*Ãí ÇÍDæP¡´PеµÚ¨Õ'2she®Ä“.Õzs²3‡úžGiaŽçþüfËsxáÿÇÍþ÷ñÃwgqg ‘9´åûTfg8øâƒl�+ÒôãÉøÔ[pìÌ2¥‚3É™C…Ùòs[æc3€˜ËëLšžSç‹\3?CÓ‹ÇN�}'#ÿ�ßqŸ´›8鼾ǒ1uü«Ü�Ñ�Hé(y¡3kè4„mÇrŽ5@αÈ9™1€Í™1€Í™0€Íž2FÛCigmzÓi„ 7@€Í™‰Îf�‡Ì`Ú…H‹ 7€´ûÆÓÏ:�wn€5ÒÎ:­©C3c›942c›942Ñd‰k€œc �s¬rŽ5@α™C'¬Î¤·aèÌ¡Æ14juö¿¸ï²G·ÇÉò™ 8ÂÉ™iguŒ¡ZoòÔ[ÿ‹I—“ç/QtœÄ2¦Žœ9ôÜwgc}„Ù-8‹îÄe=væ'Ö’ŽŸ¢ëd%s¨Äf!8õMjæÐ8ÉPæP †j†I:@´íøIÍ c»½Î©Ì:,YË:.Òˆ[Tõ'`�µ©ù}‰æÌ'94švæÐ`¦.îHëª |Ï€GæÐaÈbæÐqÕsÜ‘Öß»À×Àm qc“‡Q²^LQÜ‘Ö_à£4·Ä’*‰ªî%¸ X2O¤qX4"rø$ü Ùç®-iàhý‰ˆ�Zoa�ü@ "¢À+á—Ò³ÄE¤í+¡æbDÄSUGDÞö 5SÛDK\4 ´Ý+"{ ªB0. À€Ýá…”6Ö2^"-?~AØàm7ü:Ú‚¾�»°&˜" �÷.û>tŒ†ÿ9Opx/\Ð ‹%[Dº´ÜjÛº†ƒEÄWU#"ç€GÚNX¢ÚFâ䢬éé¶xDDÎ…Ú®ëë¹lÄ!2à‰ÈËFø¬c…BЙu(XC¤G´ÿ#=„5�>#þeÀë%>lÐù5 EÄ ß? <ìæÇˆe<\>ö‰ÈUu|½Ø´÷/º]èx;ð0ðp'p°åÊ·Ý2ËÀ‚Ý�wEähôa·v½ø?òçøBú4IEND®B`‚ic08׉PNG  IHDR\r¨f–IDATxœíÝoŒ\u½Çñ÷÷œ™ÝîÒnÛµÛF©\«4j(hÒ¢5xQ1‚< jñ6×+†kxPŒ÷Ñ}D�ü“½ÑD“›ÒPx`ôB¢¹”ˆ^µhBø òWIÿZh»ÝÌžó»ÎùíNw§ÛÙí™Ù=ç÷y%“¶ÓÙ™3û�ïçüÎïœ9Çèçœ1�šYÚtÿ*à2`3° ¸¸X Q'–Gd‰I�ãÀàoÀkÀ‹ÀAà%3;é蜋Èú"13Wô‚X‘O–7~dfIÓ}�k�/“5þú"_S¤bÞ! ‚ßO˜Ù_ý8çüJµ° ($f6¾s®¸ø:p PŸñ#> ¬i #‘%Î5ýéÿÏxLø=°ø•™�B±ApÁM看›øp'𱦇Mæ¯ñš"æÈ6Pkºÿuà~à—fvÎî½…Zp3úí|3›tÎÕ€o»€ ùCü‚©éEƇL�Þ~ìnê½Ï,¨1�s‘ŸÜsÎ}ø°%ÿïI²¦×„žHqÒüæGÏß5³?ÀÙ=9ó?ìÈ“ç^à?òçQã‹t^s8à‡Àæ£�yoÌ+œsµü…>ü7pÓ“j|‘îI™žDÿ3°ÓÌ^÷=Úî“´MÍ-°XC¶Ö¯Íý“"ÒA¾ÿl7³'æm­µ�sõ¼ù·“íŸ\C6ɧæY\5²^\üÎ9·=ïÕ™»Þ[:ï iÍ¿x ùE–¢æM‚f¶¯�‘ÀœТùý,£š_déiî϶BàœÐ4ÛÿEà‰¦ÿRó‹,]Í»¯5³'çÚ;Ð2ü>EçÜGÈŽK^�†ý"eá7Þ6›Ù[ç:N`VCçGøY>‰°ÌŸPÍ/RYÏûò^¶¼·g=p&?\øðY²Ý 3¿¤ "K[LÖ»Ÿ¾—÷ô¬>>+š¶û?Gvp�ÿ!Ë/R>Žé¾ÊÌÌœ˜jl?ôÏü,pEÓ‹H9ù~žìû: àü—‡š7â|’à[dͯ¡¿HùùM�+€oå=>Õ×Sk€~àUàChÖ_¤*ü^�CÀÇ�Q3s¾Áã|HðMàb4ë/R%~¯ÀÅÀ7ó^�áì]ËÈÖþëÑÚ_¤jü(à²QÀ8dMî×þ7Fk‘*ò£€7ùQ€¿àߘ>9¡ˆT“#ëu€ÔO~x…é¯÷j¿¿Hõøü$ð 3{#Êç¾Dvêî5¿HUY�×�/9ç,Ê·®oz€ˆT—ïñëÍÌEιÀ•ù�šü©6ßãW:çVDdG­#Û>Ð@¤ÚŒ¬××WDd×ëkÞ "ÕæwõoŽÈ®Ò+"áÙóhø/ßë#àŸfÜ)"Õæ{ýŸ"`ùb.‰ˆ,šåæœKÐî?‘¥º˜§H¸"5¿HÀºzm?çªñeC3Í—¶Cõ^úÌu¸JiêpÎÇÕl$IŠDD�Eõ.—Ž@’¤g}NŸ#Me Tç²µÁÊ•}S÷eïÇJûžŠ¤z—OGÀ?£¼ðÂ;<üðS<ýôë9rŠ$É>eúe6ƒ¡¡lÞ¼�[o½’­[/²án•‡ŠsQ½Ë«ðÈž-{Ê»ïþ5<°ŸÑÑ÷éí­Q¯ûkŒ”ìÓ0ÃädÊÄDƒz=æ¶Û®â¾û¾JOe>ó¡z—»Þ…€sÓ?·ßþ öìù k×Çiš–n-p.Ù¶ á;všë®»ŒG¹“eËê˜U{Ò¨™ê]þziêˆ"ãž{~Ã=÷üš‹/¤Ñ˜¬Ì¡•žž‡ŸdçÎÏó³ŸýëÔï ªwùë]Xø_ÄË/âê«ï¥··^™Ý@çS«Eœ81£�ÞÅ5×|rÖdX©ÞÕ¨waKí‹ÿÐC� Ž[§b6d*ï­õ{ÏþoÏž?åï±¼k„vµ[o–@ÍTïs+ì@ ¿ôÀ�×èí­“¦­×�F¹×q<{Pš:úúz8xðmÆÆÞ§¯¯Dsi·Þé¤+õ`Û¬ïÉV©Þ…€óÃÃã>|Šz=n¹hƒƒõRïCN¦vmyÎ9êõ˜wß=Ñ#§Ø°ahj-QEó©wߪZ©gRÜŒp«R½ =8IÒ–k³lÍ?8X绂�Á:®QÎD¾rÓó¼òê(}}錓¨¥©;çš°ŠæªwÒpô­®ñ/~’¾Õ5’’Ö{ïÎW8þÚõ¾WÁzç+°\´º‰›5´*ƒ8¶JÏrÏG;õî[U£°¼`çšÛ¨ˆ®~²‘€K\iGjþùI'IÕ6Ê<ÑŽ®ÀÔì곬R!m̪Ëâ)çÎK)„@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜ `!}]8¤÷ZU €‚•õì°óá¿ÖÇ‘¾â[rÕÿ´v‘sŽ‘‘ñÅ^ŒŽó§B'I4 (³®Ÿ¤êvìØÍêÕ-öbtÅðð8ÃÃcÔjq0ר@�ÌŒcÇNsèÐÉÅ^”®ˆã(¿þŸ”• `õz�žžÅ^Šîh¾6 ”“ `Î9ÍŽKihP$` ‘€)D¦ ˜& öàƒwpÉ% MQTÍÃä¦/:Æöí»9yr„Z­¦=%¤(ØÆ�ëX¿~p±£+&&Äq¤½%¦(ØØXcꪱUŒŽ¾¯æ/9@Á¢È¦¿º�õXÕ÷MŠL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"+ô„ f†µ¸\¬sÇÆðpÂWnzž(6(é™dþö÷qz{£© d6ËÞ÷—i±ÌUï(6&Î$ìÝù VâzŸ:4AÜá*ZïBÀV¬XÆÐÐrŽ9IOÏÙ×�7ƒ$q¼üêhi? ½½³/‰mf4“ °ví@~ß",\—´So \ Ç_[œ…,HÜSíz6H’”8ŽØ²eÏ<ó&Ë—÷’¦gwºô÷•{«£Õš?ŠŒññ—_¾žåË—Uú|€^;õ¨—¼Þ­ÖüUªwáÕÙ¾}+õz|ΓE¦i¹o-‰‘Ñh$ìØñ9 ¬ fž¯Þ.-÷­•*Õ»°ˆãˆ4ulÙ²��;?ÏÑ£§èé©þ9G{zj?~šnø7ÞøiÒÔÇå^ëµCõ®F½Í_þʸãã ¶m»Ÿýû_bݺœ#VãʹþŒ¸Qd?>̦Mëyì±ï°fÍ Àµœ«"Õ»üõ.4`úœñ##ìÚµ—}ûžÂ –-ë¡V‹1ó§•.òU»Ç9H’„ññ�FÂõ×_ÁîÝ;Y·ne©·Jõ.éË0ý¡xüñÙ»÷Ï=÷6'Nœ!MÝÔ‡¢š—¥fƪUýlÚ´žmÛ>ÃÍ7o¨Ä‡a¡:Yï¢ë7_U¯wG¦‡‡þ—4::ÁÑ£§I’´ãõš±±÷Ù±ã¿8vìõzëk×-äZ~CC+èË_ Ê> ,B‘õîtýæ«ÊõîØ¬�?H"I²©Ôþþ^6lêÔË�ÓààE:ôÞìýÔ¹…^Ë/MÎù  j|.D§êÝ©úÍWUëÝñi[?Cê×Ýv¾ÎB®ågæ×tÕù ¥èzw¢~óUåzwm¿ÍR=l2„kù-†nÕ[õ»0åÝ�)"L 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"Sˆ¬ÖÍsÎuóåÚâœkºµ÷3fÖÙ…ªˆnÔ{!õ›¯*×»ã�¦Yqâ8Z”_dÍýšµZŒ™Çó[¶$I1ƒ(Ò ªYÑõîTý櫪õîh$IJG@VœÓ§ÇHSG§sÀ90ƒ±±'OŽÇvεÃÉ“£œ:5Jšºó~زç5V®ìËßùû±Ž¿§2(ªÞ�ªß|…Pos§ùg4ƒ^x‡‡~ЧŸ~�#GN‘$Ù¢[iš2<<>çcV¬XÖVªûe6ƒ¡¡lÞ¼�[o½’­[/²¡h•‡ŠséT½‹¬ß|…Rï {¶ì)ï¾û×<ðÀ~FGß§··F½“­:ÝýÙkdCù?I’Î{[ur2eb¢A½sÛmWqß}_¥¿¿§2ŠùèL½;[¿ùªr½ ç¦'~n¿ýìÙóÖ® Ž#Ò4íÊZö2Íý¢ )`¶-˜ K�;Íu×]Æ#�Üɲeu̪=iÔ¬õîDýæ«Êõ.tìä\¶vï½ÿÞ=æâ‹WŽÉÉdjr¨Û·v–y¾·4uLN¦$IÊ?¸Šýû_â®»öN}HBÑ�z·³ �¾U¹Þ…�ü$ÌË/âê«ï¥··Þ•Ý@KA­qâÄ�>z×\óɦɰêR½«Qï–Úÿ¡‡0::qîÝ2fù,j9o­ß{6LܳçOù[,ï�°]íÖ;"—÷Öú½W§Þ…íô3±¼Foo�4m½6H'“¢^rQD-’>M}}=<ø6ccïÓ×W� ¢¹´[ïFRîQA3«X¥zþÍ�søð)êõ¸å¶‘™Ñ7Ð_Ê_”71:Ž›ñawÎQ¯Ç¼ûîŽ9ņ CSk‰*j¿Þ0xQOVï’æÀðxƒÄ¹³B Jõ.ô@ $I[® ÌŒd2¡o Ÿ�÷ÿ;}ý$“I)ƒàÁ]?çø[G¨÷öÌÚæMSwÎ5a�»ÞÙš°¿—ß캆Áþ]8¬nù铼zø}=5Ò Ö»Ð8_�ý åE¥ +øh³2;½³À.ꥑ”3b³²^ÚÒÕ/A6�ä·2€ÌO#qS·2–»ÊÍ‹3÷Hµµ3«.‹§œ;/E¤ ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` €‚r^L ¬÷ZU €‚•õì°óá¿Ö›]ÿoq—E.Lõ?­]äœcddîKYUAšfŽŒŒ“”ü¤Ÿ¡ëþ A*nǎݬ^}Ñb/FW �3<}_Í_r €‚E‘M5~u ;걪ï/$š ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)DVè AÌ kq¹XçQ11:΃»~Ž•øD§Ž¼G\o}þ»ìý/ÂB-’s×âÈopËOŸÌNRÒ3ýýÝzk1iEë]HøÁŠËZΑ#'éé™}ýx—:Ž¿u4?¥L¯Ü}q½6ëCof4“ °ví@~ßb,]w´Soçxõð©²ö>½µxV-«TïÂFI’Ç[¶là™gÞdùò^Òtvéë½õ¢^rQ´ZóG‘1>ÞàòË׳|ù²JŸÐk§Þôõ”û¬s­ÖüUªwásÛ·o¥^�Ïy²ÈìÚyå½µü%FF£‘°cÇç¦Þc(ÎWïÔ¹RßZ©R½ €8ŽHSÇ–-عóó=zŠž’§;zzj?~šnø7ÞøiÒÔÇÕŸ[U½«QosÆ—¿2îøxƒmÛîgÿþ—X·nçȇ‡Õ¸r®?#nÇ�³iÓz{ì;¬Y³p-'ƪHõ.½ ˜>güÈÈ»víeß¾§0ƒeËz¨å*Ùi¥‹|Õîq’$a|¼A£‘pýõW°{÷NÖ­[YêmÁ…R½KúÆr…L(üEöî=ÀsϽ͉gHS7õ¡(#3cÕª~6mZ϶mŸáæ›·TâðPªwyu$`zxèI££=zš$IK=dZÁÀ@à?ØåAõ.'sÎ%tðˆÀ$IJ=QÒJšf{ªö¾.”ê]*©9çŽCdÇju,Öü¢ ̨Ì STï%Í÷úñp†,:ª ‡MJûTïR8oçÿ¨H^‹Èyø^;þ:ãN©6ßë�€sIDdѼ��@$YÏŒ€ç�£d³‚Ú ©6¿à(ð|dfÃÀ3ù¦‹¶X"Ò ¾ÇŸ1³áÈ9gÀoó;5©6ßã¿uÎe{j�s^aú!Úƒ+R=¾ù'�O˜Ù‘s.2³7€?æÿ©Í‘jò½ýG3{Ã9ELÏþÿ­ùEªÎÈz ²|`ð*°žl¨ Ý‚"Õ‘’5ÿ;ÀÇ�q€ÈÌ›Ùðc´;P¤Šüî¿ç½›™ó“€~ÐO6 øˆT…_û"[û�˜™‹ü_Èaø~þ`MŠTƒ€ïç=ç=?=é—� ˆ�g�+€$ÿ·ˆ”“ïáç�-ù¿�€©!~~‡™Y¸ƒlÀßD¤|š{øŽ¼·Í7?ÌØÆ7³Ä9W3³ÀÉ Jº¸À"Rœ„¬‡hfòÞ>«Ÿgí÷Ï7üñÿ|m ˆ”�ïÙ§�&›H›×þpŽò£SçÜGȾ.¼í) ?é÷°ÙÌÞò==ó�-:oþØÌÞnaz@{D–6ߣ¸%oþ¸UóÃkô¦ù€'�¯5=V! ²4ùÞŒ€¯™Ù“­¶û›�÷Øÿü &�sÛ�‡˜žUÔæ€ÈÒá‡ýì0³}¾wçú¡ó6qÞüu3Ûì ›\ˆÐÞ‘¥¢¹'}ó×Ï×ü0�oÿ5�®ökȾW\ýkB‹,]¾ÿl7³'ÚYó{mã󿝙ÙÀVàÏù ;4/ Òm)YïÕÈzqë|›æ¹Ÿ‡@lf¯_~Ðô<“(D:-%ë5ß»?¾hf¯ç½ÙvóÃOÒ¼OÑ9÷àGdÇÓ´pš$)Nšßü&÷³ÀwÍìpvOÎǂϔ1ûMàÛÀ.`Cþ?I]È눬yóÚ‰û&ð`wSï%3�ðk×7f>ìHò¿ßî>Öô°Éüµ"sóMï·ï½×�û�_šÙi8»÷ª�fôßh ‚~àfàëÀ5@}Æ�ø…öû- [‘’pMú¿Ïü¾Mø=°ø•™�BÖø´8®! mº™A�ß·¸ø2°™ìœƒ"ÒÚ;dß¿ùð„™ù‹÷Úø^GÖº~~€laÓ¦ûW—‘Á&àRà`90„&% )p8ü x�ì"½�—Ì줠sÎO¨/x;.ÿ6]Üœe¼vÜIEND®B`‚ic09+ЉPNG  IHDRôxÔú+�IDATxœíÝŒg}ïñ÷3çì/Ûë_IH\JpB‚pn!-Ä�6�’à^ PE*j¹…PhU©TâRjø'¨ ˆJAU"U¹ ê­€7Ð$p“…RRBâüTHBpü{×»ëÝsæ¹ÌŒ}llǻ޳gæ<ï—´ÙìîÙÝñžó�ïgžyæ™À‰1 Ð !Äc¾>lÎ^l)ßoÖç”=h¯ÌVK’ú¬ì*ÿÿ9`ðKàqà±òýÀ/C ½ßXö•�üؾÒdaÐpºzš>!„î1_û àbàµÀ«€‹(šýÚ•ÝJIRÍ ?~<BøEïƒbŒ­òNÔôcŒg—W¯¶'ø19E¢ƒ#‡cßK’†C<ÁûÃýä8f�ÿîîî!ì>üÕèbŒ�…:=Ÿ;xp ppÆ1ßÖ¥x¢Ã1o’$Uâ1oÕЯÝÀ=ÀW€o‡ž;üÍ1¶)‚@¾2›{úÑËÆª£ýã*à€?Þlèyx^¾Usño”$ÕNªžÒ;R°ø7à6à!„8<*›jÝËÆOõ‡Œ1¾xwùv^ÏC«ÓUÓ—$i¹UaŽx¸¸5„°~½ÕQ-›åqÿ6àCÀ;€ñòa6}IÒ / ÌwBøw¨w¨Uã¬&÷õ õo>LÑø+~}(F’¤A©N=÷^>~ðéž Ð¢f“kbŒíjr_Œñw€�ptãïâѾ$©¾ªQ�ÞÓwŸ !üÝëmàÍ´‰!„c|1p#ð^Š$u¼?¦$Iu×{ÐÚ¾|"„ðL9Ú}Z`  JBåãCÀÇ(Váƒâ�gã—$5Yo/Ûü Å�8èÑ€�€cŽú_ |ø½òË\†W’4\z{Ûw�‡~0ÈÑ€ŸHW&ž¼lþ7ߣhþŠ!›¿$iØT§µ;=ï{1ÆC1„�— ­¨¨n¨PùŸOq>äòòË÷K’RÑÛóîÞBx´ ¿v#»~Y‘€ž•ü:1Æwß§hþÕQ¿Í_’”Šê^øýã»Êù¡Z; ßúþKbŒ­òÜFŒ1ÞÜl¤H@mjp%‚$I+,PôÀ.EO¼½ì‘Õ)�¾÷µùöÌòß@±^òÿÄëù%IêÕ{Éû�À‡ööû*�¾5ážæÅ�“¶ ÀH¿~§$I VõÈÿ® !<ÒÏЗS1Æ‘²ù¿žb‚ÃVŠs6I’Žo„¢Wnî�1¾¾ì¥}é�Ë>ÐsäÿàkÀ*œå/IÒ©ªzæ ðÖÂÝý XÖ€žæÿNlþ’$-E‹¢w®¾c|gÙ[—u­€e(‡ýbŒ×PÜüŠI ÞµO’¤Åëí¡ï!|¥êµËñ×%3ìÿu`¬ü’Í_’¤¥«–>¼e9Oœv(¯óïÆ/ðÈ_’¤åQõÔàÊÂýUï=�zZ §ùŸO±¦ÿÙxÎ_’¤åVõÖ_¿[.|Z!`É º£°¸x6I’ú¥ê±?^ìç4î$¸¤aúòÆ>Yy¢hþlþ’$õK‹¢×¾ø§²geO^´¥ž§¯îêw°�bõ"oã+IRµ)zîöãMådÀ%|/:5ôœ÷¯.÷«~¹kûK’Ô‘#7Ô«.\ô|€E5ížóþ/~ ¬-¿äŒI’VNuÞÿp ð$‹œpÊ�»<ÇÊs·ë)€Í_’¤•U�¯n-{sXÌ|€Å4ï¬ú¿‘bö¡“þ$IœjRàëbŒ7–§N¹¯ŸRRè9ïÿ*à‡å÷e§úý’$©/"Å逼&„ð“S�ð‚I¡N(ßž#³ýmþ’$ VÕ‹ÛÀç�éÙ'u*CY™$þ ¸û‘$©Nª»^ üÙ©ž 8iB(gýœ ƑՆ$IRýU«ô~ìd£¿6гèÏoSý»Ü¯$IÍRM|MáGÇ[èd�ý¯92«P’$5GþúD8j Ƙ…òãyÀÏ�Ñã=N’$ÕZ5ô?¼"„ðDÕã«;P}ü¿€1Š•…lþ’$5K èác=Žéù‡›{ϺÁãÀ`3Å‚çÿ%Ijžª‡? ¼˜ƒÃ“ü�jî­ò“Û)šÿ¢n+(I’j%£èå›�íe�oõ~±R�/øެ),I’š«êçÒó1Pžˆ1†BŒ1ž <¬-äùI’š«êå€-!„]UϯFª!�íÍßÉ’$5_5p-E�‡²çW x'ÿK’4Lª¾þΞ� =Ãÿë�'€õ8ü/IÒ°¨zú>༾cÈ82 ðzŠæÿ‚÷–$I�Q­ê»ž¢×d½àªò½KÿJ’4\ªÞ^õú,:å¿[}rE7I’$õ[ÕÛ«^ß©.|1ð(Å*€žÿ—$i¸T½}8?„ðL•^EÑü½üO’¤áS]8NÑó ¼¦|ïå’$ §ªÇ¿Ž€KÊ÷ýK’4œª @clQÜ%¨÷‹’$i¸T=þå1ÆV¼¸|ëý¢$I.U�1ðâ Ø¬ÁÙÿ’$ ³@Ñë×[2àeå\H’¤áVõú—õI’”†—eÀK½’$iE½4Î-?ðü¿$IíêõçfÀÆAn‰$IZq3à¬òG$InU¯?+ÃÆ/IRjBˆ1.íAo‰$IZ1�cô@’$%&{á‡H’¤ac�$)AI’d�$)AI’d�$)AC{ýqq£W8ÖC ¸Ü”–‘õ]'ÖwS Mˆ1’çÅ!Ë!\ä°>b„›qèP‡ÙÙy“é€Åcc#LLŒõùn7'„@–ùéĬïz³¾›­Ñ#Ýn^î»wOsï½qÿý�òàƒÏ²s禦æÈ²@³#Ns…PìÀW­嬳&¹ðÂMlÛv>W\q!çž»(žC�t<Öw½YßÍ×È€<�‡“å#�üŠ/|á;|ýë?å©§v‘‘‘ív‹,Ëð<á ò<§ÓÉYXè‘sÎYÇ•WnåúëßÈ%—ü&pôsª´YßMb}7Yã@žçdYF·›sÓM_糟½‹={¦™œœ`t´}8•6ë_5üB€ŠÉBóó]¦¦fY½zŒ÷¼çrn¼ñjÖ¬ï9âSª¬ïf²¾›©Q z=óÌ^Þÿþ/q×]?cãÆÕŒŒ´évsôOIZÐjet»‘Ý»§¸ä’—rË-×ñÊW¾Ø�D¬ïá`}7Gc@5„ôÈ#ÏqÍ5ŸáÉ'wqÆkètº 4ØÈH‹ýûg™œçöÛ?ȶm结H�õ=œ¬ïzkD¨vÏ>»�íÛÿ–§ŸÞ˺uå9'5]»�1;;ÏØØwÞùa¶nÝì9ÄXßÃÍú®¯Úǰ‹Ë€º\ý?ðä“»Ü9 ™N'gbb”ƒqÝuÏþý3ù&Àú~Öw}Õ>äyq ðÍ7‹»îúgœ±Æ�ÃêtrÖ®�à�žæãÿ Y(×ѳ¾Ó`}×S­Oäy$„ÀO<Ïå—²œ!ìu¿Ã,ËâÎ;?Ì¥—nñ|á³¾Óc}×K­ÿò1FB€ÏþÛìÙ3ÍÈHË�Ã� !Ðét¹ùæoþXÃÉúN�õ]/µ ÕòŸûöÍðÕ¯þk×NÐíºwvÝnÎää÷Üó�=¶³*ôy6Öwš¬ïz©m¨^÷Üó0O=µ›ÑѶ“FÑngìÝ{�o|ã§î †�õ�.ë»>j*÷Ý·ÃCbb„v»Åý÷? àåBCÌúN�õ]µ½Põ¢xðÁgi-ùçxŠi°–²o�126ÖfÇŽçètº´ÛKþUOËUßo ·Ò ´½ÒÉ\{íç8óÌ5„½)Zf{ödjj–v»eÈ“À ZÛ³ç ;wôf¨ÚíÌÓÒTk##-FGmÃ(F'JƒdP­Å±GHÒòs $I 2H’” €$I 2H’” €$I 2H’” €$I 2H’” €$I 2H’” €$I 2H’” o¤Z»õÖxÉKÎ Ï#Y½9Z1FBLMÍríµŸcß¾ƒ´Ûmï (­0€jí‚ Îfóæ�ƒÞ õÁ¡C ´Z™w{”Ä Z›�] Ï£#C¤˜™™·ùKdP­eY8Üø Ã!F!ø|Jæ$@I’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AíAoÀÉĉ1.áû Õ LMuyëÛJÖ °ø£eðÔ/æËÈóÅïRŸ5ÃéÔwÖ šîrÛŸ>D°¾fÿ³‡h�fD뻑jÆÇGYµj”]»ÿ" ºÝÈÏžqç0@cc!,ö»yž311ªU£ýØ,ÕÀéÔ7bÏ?:»ü¦SÖµ¾›¬– „@Œ0>>‹^´–Çžññ"ù/îçÀª Ïr ÒRŽü³ æç»lذ�3Ïœ<ü9 ‡åªo€ë{ –räo}×G-@žç´Z^¸‰ï~÷aBg)‡òKi@¬óó¶l9‹ÑÑ6yɲEf¨Æ–«¾—Ò€4XÖw}Ô>w]vÙK:2Ps… ].½t yî `XYß鱾룶 J„oxÃElÚ´ž……îÎ5©‰ºÝœÉÉ ¶o¿À£ƒ!d}§Ëú®�Ú€ÝnÎÙg¯åÍoÞÊ�³´ZµÝ\-“V+cjjŽmÛ¶°uëf‡‡”õ�&ë»^j]qÕd¡|àM¬^=F·ëPQ b„}èÊòÿ}·•õ�&ë»>j²¬¸\äâ‹7ó¾÷]Á®]SŒŒ´½Yê“v»ÅîÝÓ¼ýí—pÕU[ÉóèQá³¾Ób}×Oˆ5�`Åb03sˆíÛoâÁŸeíÚq:§ÿ“V+cnn��Wó�ï|„M›Ö ¾8<8Ô¬ï4XßõTûøÊ™AkÖŒóÅ/¾�ÉÉqfgçi·k¿é:E­VF§Ó¥ÓérË-×qî¹<7˜ë{øYßõÕˆ*˲bÂÐEmâöÛ?ÈØØSSs�v»ÅÜÜ<‡uøò—¯çòË/¤ÛÍLˆõ=¼¬ïzk̳Ðjet»9Û¶�Ï×¾öWœþÙìÜy€, ¾˜¨ÕÊhµ2vïžbãÆ5ÜqÇŸsõÕ¯vç�(ë{¸XßÍPû9Ǫ^@û÷ÏðÑ�ÞÁm·ÝO§Óerrœv;#Æja‰è#5QŒò²,¾GÃôô1FÞö¶Wó©Oý›7otç ë»�¬ïæj\Ž:ô½ïí೟½‹{ï}˜½{Òn·kÓngdY±Ãp�‘Á¨þöyétºÌÏwYXè099Î¥—ná†~Ÿ·¼å·Ü9è0뻬ïækd€#·’ÌÊ»H<üð/ùæ7àûߌ;~ÅÞ½™��'„¥ÝdD§¯úÛ��µY¿~5[¶œÅk_»…«®ÚÊ«^õ Øy„pd2˜ÖwXßÍרPÉóœjø©2?ßaÏžƒLOÏ å /ÆH�éé9®½ösìÙ3ÍÈH{I‹jÜzë ¼ä%gômVnŒ‘U«FÙ°a5£G}Þë€õBêXßMª¿~³¾›­¶wGÅ䮿íð´²ê^ßMª¿~³¾›§ñ R½è *ÄáŸ$T ­�ÞϘ�]8ªpû¡w’P�r4xu¬ï¦Ô_¿YßÍ54 Wõ‚ÂÑÿe×[´¯š`˜êÛúÓ y‚F’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤µ½ý#@ôfÔ^Œ±ç­_¿%B¿~¶R4,õ½2õ×oÖwS Mˆ1’çEeY „øª|!ív‹­VÿV1Bžç@ïó#�ša­ï•ª¿~³¾›©ñ Ï‹ôÜjeGÑ¡Cfgç‡2™Æ !0==Çž=i·³%=ìÛ7Ãþý3äy$Ë–ÿ�#Œ��011B«uäŒS·›BèËïÔð¨c}7©þúÍún¶csžºÝüð‹n÷îiî½÷!î¿ÿQ|ðYvî<ÀÔÔY<´vr1F˜=­Ÿ199N–õg*HÅ|ÕªQÎ:k’ /ÜĶmçsÅrî¹€â9ôˆAÇS÷ú®{ýõ›õÝ|� ½iù‘G~žð¾þõŸòÔS»�ÈÈH‹v»UVãþy§ Pü»íöéí<ºÝœþ¾yžÓéä,,t�È9ç¬ãÊ+·rýõoä’K~ ±G@Z~õ¯ï&Õ_¿YßMÖ¸�ç9Y–ÑíæÜtÓ×ùìgïbÏži&''mN¥ÍúW-Ýé>}+•ÌC(~W0?ßejj–Õ«ÇxÏ{.çÆ¯fÍšñ£Žø”¦¦ÕwSê¯ß¬ïfjT¨^@Ï<³—÷¿ÿKÜu×ÏØ¸q5##í!HÒéZ­Œn7²{÷—\òRn¹å:^ùÊ»“H˜õ=¬ïæhL¨†�yä9®¹æ3<ùä.Î8c �N·6GZ¼‘‘û÷Ï299Îí·�mÛÎw'‘ ë{8YßõÖˆPíž}vÛ·ÿ-O?½—uë&ÊsNjºv;cvvž±±î¼óÃlݺÙs† ±¾‡›õ]_µ�aÕ" ]®¿þxòÉ]î†L§“311ÊÁƒ‡¸îº¿gÿþàôϯªþ¬ïág}×Wí@ž×ß|ó·¸ë®ŸqÆkÜ9 ¡N'gíÚ xài>þñ¯�e�r] 1ë; Öw=Õú@ž n<ñÄó\~ù'ËÂÃ{]¿ŠUÄ<Ä�w~˜K/ÝâùÂ!f}§Çú®—Zÿå‹·àóŸÿ6{öL32Òrç0äBt:]n¾ù[‡?Öp²¾Óc}×Km@µüç¾}3|õ«ÿÅÚµt»î†]·›399Á=÷<Äc�í,‡ }Þ‡�õ�&ë»^jªÅ=÷<ÌSOíft´í¤‘D´Û{÷äßø)€;ˆ!d}§Ëú®�Ú€Ê}÷ípÇ�˜‹»¤Ýÿ£^.4ĬïôXßõQÛ»V/Š|†‘‘ÖÒPCpÓÐæZÊÎ=ÆÈØX›;ž£ÓéÒnŸÆó¯ZZ®úöò`-%»YßõQÛB`~¾ÃsÏ('-í(!ïxIÑ eK˜á[!dìÞ}�]»¦9çœu‡oÁªá°\õ½à¼��je‹?À²¾ë£– z1ÌÌbzzé·ü !0±v•/¬:43G\ä9¾#YV¬6==¬#F�ö†ÅòÕ7l\=ZÔ·9` ¦æèƸ¨`}×G-@¥¸»Ôâ_!º�.kWñ§÷&Ö®¢ÛéàÖ¿¼…çŸ|Ž‘±ÑEåUwÓpZz}GþW�ñþò�l\5ÊBm ðŽÏÜÍÿÜÏÄh›ÜúnœZ€ÓU�¬Z·Ú0 Á >ê“jàŒÕc,t ƒÐ ÁÁ—êÅ€nùf�†ËB7~³¼WžÍ¿Ù†>P3.u¸QR}ÃÈGÞ$�ºÚ¯ I’–Ÿ@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@’¤$IJ�@µcô&¨O|n¥Á2¨ÖÚíÖ 7A}âs+ –@µ¶ÿŒGŠCjÿþòÜçV”ö 7@:™k¯ýgž¹† 7EËlÏžƒLMÍÒn· yÒTk{ödçÎƒÞ õA»�y@ €jmd¤Åè¨MbÅèD@i� ªµ#öIZ~N”$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’d�$)AI’äÍ€Tk·Þz/yÉäy$Ë 7GË ÆH�©©Y®½ösìÛw�v»í�¥fP­]pÁÙlÞ¼qЛ¡>8th�V+ón�Ò€Tk³³ äyt`ˆT#33ó6i€ ªµ, ‡¿`8Ä!ŸOiÀœ(IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%È IR‚ ’$%¨=è 8™#1Æ%}_ÖÊ843Ç­y ! }Ø:�ŠýÏí¥5Ò^òó¸”ïS3,½¾¡•¦æxÇgî&Ëø2ˆ_ì9ÈX»En}7R­Àøø(«V�²k×Ò^$1�<ÿ䯊=†` Z#mBXì?�ç9#¬Z5Ú—íÒà�N} #ÿr¿½€ÆÚ-]ÞÖwmÔ2„ˆÆÇGxÑ‹ÖòøãÏ3>^ôñÅYþ Ô)[JÂÏ2˜Ÿï²a÷˜š'„Àü|‡-[Îbt´MžÇb˜WCc¹ê{) Hƒe}×Gís×e—]°¤#5W°°ÐåÒK·�羆•õ�ë»>jªDø†7\ĦMëYXè.á\“š¨ÛÍ™œœ`ûö‹<:BÖwº¬ïú¨m!Ðíæœ}öZÞüæ­80K«UÛÍÕ2iµ2¦¦æØ¶m [·nvxpHYßi²¾ë¥ÖWMúÀÞÄêÕct»¥ FøÐ‡®,ÿßç|XYßi²¾ë£Ö ËŠËE.¾x3ï{ßìÚ5ÅÈHkЛ¥>i·[ìÞ=ÍÛß~ W]µ•<�1ë;-Öwý„XóV,33‡Ø¾ý&|ðYÖ®§ÓɽiZF­VÆÜÜ7®æ;ßù›6­'FÏ;ë; Öw=Õ>~U‹È¬Y3οø>&'Ç™��§Ý®ý¦ëµZ�N—N§Ë-·\ǹçnðÜ`"¬ïág}×W#ª,ËŠ C]´‰Ûoÿ cc#LMÍ9\8Úíssó:ÔáË_¾žË/¿�n7wh0!Ö÷ð²¾ë­1ÏB«•ÑíælÛv>_ûÚ_qþùg³sç²,øbj V+£ÕÊØ½{Š�×pÇÎÕW¿Ú�C¢¬ïáb}7Cíç«zíß?ÃG?z·Ýv?�N—ÉÉqÚ팫…%¢ ŒÔD1ÊȲ@ÐíF¦§çˆ1ò¶·½šO}ê�ؼy£;Yß d}7WãpÔù£ï}oŸýì]Ü{ïÃìÝ{�v»ÅØX›v;#ËŠ† Œ Fõ·ÏóH§Óe~¾ËÂB‡ÉÉq.½t 7Üðû¼å-¿àÎA‡YßÍ`}7_#¹•dVÞEâá‡É7¿ùßÿþcìØñ+öî=Èìì°;°Í”$IË©KÑÛï.›+„«!�êhÿŸËÿ÷è_’¤áPõõîù¸øOÏi€3)î °OH’ÔtU/?l !ìªz~Ðs`ðm< IÒ0¨†ÿ¿]6ÿV!ÂÑ3þ«£ýÄÓ’$ ƒªŸÿcÏÇGÿOŒ±úÿqŠ›m¦¸DÐË%Ijžª‡? ¼œb@~m üD+„0 |©ç›%IRóT=üKeo?<üÇ ódzBc<ø90z¼ÇI’¤Z«ý<ðŠÂU�¯pÔð~Ùü[!„'€¡hüN”$©Yº=ü_Êæßêmþpòóû7Q$çH’Ô,E¿éd8J¡[&…ÿZ>ÆQI’š¡KÑ»ÿ5„𣲧ÿZ?î¹ýc‹bòÀ«�–Ÿv$@’¤ú«†ú_ü'�/·©—ÌB?¾Š£’$5AuôÿÕ²‡·ùÃIf÷÷Œ\ ü¨|lv²ï‘$I)úv~x€“€ë÷Œüø2ÐÂQI’êªKÑ«¿\öî6x�£ùß-ðLà!`.,IRÝÄòm?p° ŠËûOô '�ØW~c!ìn,ïꀒ$ÕKµìï�eÏ'kþp Gòå=ªÆ?p)G†$IÒ`U=ùûÀe”=»wÙßãyÁKûªP¾¿è”_:é–$I}WõâpÃ1=û¤NéÚþžÅ�~ü N”$©ª£ÿ¿ !üäD‹þÏ)Oæ«N”aàÿ¯ÃS’$ JÕƒï !¼¾º|ÿTŽþa‘³ùË«"ðRàÇÀÚòK®(IÒÊ©&ø.žä&þõZTã.pVÞ-ð½™è|I’VFµàO¼·ìÉÙbš?,áȽ<Ð!|ø4ÐæÈÄ@I’Ô_ŠÞûéÂWÊž¼èyyKZЧœÐ !tbŒß¶÷l�$Iê�ª×~3„ð1Æ6Ð=Õóþ½–¼¢_Ï|€uÀ}À+pR $IýRõØŸSLÄßÏ"Ïû÷Zò佞ùû€«�_áå�’$õCÕü\]öÞEŸ÷ïuZ³÷{ÖxøC`¦Ü@— –$iyä½uøÃ£‹¹ÞÿDNûò½žI�÷ofË/$I:=U/�ÞB¸©“þ޵,×ï—“GBwï.®7’$iéªKý2àÝ!„»Ë^»,WÞ-Û>!„…žËßE1T‘áœI’«KÑCg€wõ\î·°\¿`ÉWœH¹��〯«ðêI’NUÕ3g(†ýï®zërþ’e_Â÷˜Óož£ø‡¸X�$I'סè™Ïo^îaÿ^Ë>Pé ¸ø °XFúõ;%Ij°ªGþ7pMá‘~ùWúvŸ²ù·C�—wRüúxïI’*‘¢7ŽPôÊËûÝü¡Ïwñ+C@+„°—âÁOS mœ(IR—¢'¶(zä[C{ËÞÙ×Sç};Ы\6˜Bc|ðy`#GÎu¬ÈvH’TÕQØÜBøß½ý²ß°b�÷˜�|‘âÔx•€$)½=ï^Š[ú>z:7öYоžèBˆ=ó !\|ŒbÒCu•€s$IÃ*rdä{øXኪù‡:+Õüa@CïÕ�C1ÆøZŠó¿W~ÙÛ K’†Mooû.ðáÂÊÑñ%ßÑït¬Ø@¯B^6ÿváÀÀ_»8òr’ $©éª^Ö¦èq\Q6ÿv9:>�eó>ùî˜Ñ€7ï¥øcEŽÜI’¤¦¨–ò Gÿ_>Bxf�Gý½*½×;ÆøðŽž‡ôþ1%Iª›ã´Þ|*„ðpt¯´Z5Ó2eÕmcŒÛ€stèpäîH’$ Z^¾õÎ_»øtáßbŒ- _ÉI~/¤V rìu�eøE/V�WqT@’´Òª£}8rÄ?GÑøÿ®§ñ¯Øuý‹UëÆyœ ðràÝåÛy=5 H’úíxMà àVàÖ¨wã¯4¢Y–ÈÐsj`ðÀo6ô<¼Š©Â@#þ�’¤Ú‰iúÇžzÞ üpð� êØÌþÅhTs,ƒ@Ö;�"Æxð&àŠË Ï8æÛª›…cÞ$IªÄcÞªõù{íÃí·CÏþæb¿¼ �¿ÒÈFXM¨FÊÏŸ\\ ¼ŽâÄ'ø19GV 'x/Iñï÷“㘥¸5ï}À]Àý!„݇`q´5›ÜwªßèNʯýp1ðZàUÀEÀ&`íÊn¥$©æ¿~üx „ð‹Þ5½é÷j|èÕǹ¡BŒq„"œ¼ ØR¾ß¬Î)z&.G,IâC± ÀsÀ>Šfÿ8ðXùþ à—!„…Þo¬ndG9 éM¿×ÿ<…x¡.5ÚIEND®B`‚ic10Z_‰PNG  IHDR+ƒZIDATxœíýy”dgy'ø߈̪Ê*-%Y»„Øm,;ƦÁmŽÖÁ/H»�ÓtûðB7ÆÆÇî™93cŸ10†ÓÆtÌüŒ‡ÅÐln#Œ±{°ñ‚‘�f!³iEªRIµfÄûûãÆ­Š*•JUY™ù~>ç¤T™™y+³n<÷ùÞw)aS¨µ–$%É ÿP)et‚çoMr^’ ’\”ä²$&9wòç3'�o�<§&Y˜| ØnK²œ®¸%É�ÉÇö$ùF’Û“Ü<ùóM“çÜVJ9po_°Ö:œ|½$§ë-êZýX?徟ÂF4iøé~‡ãRÊø8ÏYH×Ü_žä¡I¾3ɃÓ5ø¥kæÏÌ‘ÐØ¼Æé‚�ÛÒ…ßHò•$_Lò¥$_KrS)eùØO¬µÒõ 5]ÿ!˜C€9qLÃ?:ö„«µ.%yP’G$y\’+’<,É¥I¶Ý×—O÷b0­œÄŸ€Ùª'ñçäH/q"û“|3Éç“|6É?&ù§$×—RöõM»þd�À\ÑÌmPÇ é¿ÇþZëEI“ä{&oW¦»³¼ßiM2= ó´¡óÖ›öìó¿‘äÓIþaòöÉRÊMG=éÈS60Íß2}—ÿØa7µÖ ÒÝÙJ’''yd’³�óeFéNRM>p*Ž ú»üÇÚ�äº$Mò×Iþ±”rËQ_¨›ŽltÀ£1œ±©¦?Ó‹öMÞ¸2ÉÓ’üp’Ç&9ç˜OOÞ4ûÀZ86èïôO»#É'’üe’&ùôqz›D0sšÅéWÖœ¾Ó_kÝ‘ä{“<;]ãÿÝÇ|ÚtÃ2sxV[¿†Ø½ŸK\“äïK)wþÄÉÈ€íXÆÚÑ@®£~^Ì1Mÿö$ÿ2É�¥kúï̧õ[zhø€�h:X8æ±NüI’�”Röþ¤. 8îŽf¬ å;Áÿ'%ù©$?šä�SŸÒßå?^’°ÑÝ[OóÕ$ÿ5É;K)ÛÐ�õ#X#ý¶ÇÜí¿’ŸLru’ÇO=]ÓlF÷Öë\›äíIÞUJùçþƒ“Q÷ØöœÕ!Xe“aþ¥¿Û?yÿ‡“¼0É3’œÑ?5ÝŠýš~ }0½åà]I>�äÿNò—ýt€É¨€jzÀꬒã4þç%¹*É¿Iòè©§.GÓ´­¦× øT’7&yG)å¶D°Ú§iÒøg*©úîtMÿÿ”ä¢ÉÓ¦WÈô3èô ^;-ÉMIÞ–ä�¥”Ï%÷ì»XÍè §ñÿÞ$?Ÿn~ÿÖÉÓF9ú2Ç×ß8í<�n�€ÿ\JùûDpº§è^ÿ_IòSO[ÎÑóZ89ýziÓÓÞ�äÕ‚€Ó£A=IÇ™ã¼Æ¿_ÔÏÏàôôÓ†S;6°FÀ)Шއc·ó«µ>1ÉËsÏÆxœOàôÛs½;É«J)Klx²'PkNÝñp’ßHòütwù�—F°v¦G]�“üQ’ß*¥|%9º‡ãžÇ1î_K)µÖúIþ]’_Jræä)îøÌÎtO¶'ÉLòšRÊ·'£¸‹i÷$˜2ù‡2˜ºëÿo“üf’Ë'OYÎÑ Q0;Ó=Ú×’ü¥”?H¯06-àÀÄ1Ãý¿/Éÿ™äû'[Õ`c:v×€¿JòŠRÊß%¦Lk¾¡�^Ý¿Özn’ÿ5É/¤ûÙXÕ`>L¯ÓV“¼.ÉÿZJ¹Ýn�¦ÛZëÂÔêþW¥»ëÿ€Xà`^MßȽ!Ýh€w$G÷€-j2˜žë_k½’ßIrÕäaóüæßto÷Ž$¿VJùç–×h.8f®ÿ ’¼*ÉyéR¢’.)`þ�Ó�ð&¹-ÉËK)oJÚ\ ™`r×XJY®µ^�äµI~zò°»þ›×tÏ÷ÇI^ZJ¹¥Öº�dÔÊh€&€ÉBµ”Rk­ÿCºÅ .�EþZ1½ÖÛ×’üB)å}“›Å¥…7ýp÷É"ã$ÃZëï&ùÓtÍ¿­ýÚQÒõ€ËézÂ?�ôˆÃRÊx2`SÛ´Íï1Cþ’äMIþEºÄ'i üฦû¿Iò‚RÊ—7û”€MÙO†ü—IóÿSI>–®ù_N÷wÞ”oNJß.§ë?Vký©É�eÒSn:›î/5YÉqÜý±¾*ÉIrnºùþ›~H'm!]¯xn’ÿ2é!ëdJÀp¶‡¶ú6Õ€É|ÿåZëùIþ(É3b¡?Nlz�À$y~)åÖ¾Çœí¡­žMÓO5ÿ�Oòö$Žíý8y}ù•$W—R®ÝL!ÀÜO¨µ–©æÿ¹I>Í?§n!]/ùà$®µ>wÒk.LšŸks³Øß+’¼#ÉŽtC74ÿœª…t=åŽ$卑¾b³,8· F­u0Yì/µÖ×%yIÌ÷`uL¯ ðû¥”_HŽîEçÍ\6Ê“•þGµÖíIÞšäÇÒ ÓfNÿNl85Gv”û“$?SJÙÛ÷¤3=²˜»fyªù¿ É;“|’CIg{dlR}ÏùWI~ª”rË<†sL5ÿ'ùoI®ˆæ€µ×÷žŸMòC¥”ç-˜› &+ý�j­W$ùhºæ9šÖÞbºôŠ$­µ^1éQçfú¹0µÍßIþß$¥›‡1œí‘И¾½)É–R>Û÷¬3>®û´áGhþØ@†ézÒ‹’ü¿“‘Ëó0`Cš6 ¹ 6쀩ÿ4ÿlDÇ›°aÜ�#4ÿÌ�ã�ÕZ7dïºáFL5ÿ%ù»$ˆæ€�«ïYoHò}¥”›6âH€ 5 Ö:˜4ÿç%ùoÑü°ñõ#�ä¿ÕZÏ›ô¶ªçÞ0Óÿ`j­Û“¼7ÉÃÓí±¨ù`£¦ëažä½“Þ6)ØRk-IJ)eœämIžœäP’ ½‚"LYH×Ë>9ÉÛ&=n™ô¼3·!€$ý¼ÿ×%ùÑt?°ÅœªÅt=í�ÖZ_7Y`CŒlŸyPk]˜ì—økI^Í?ó­^RkýµIÏ;óî3†0Õüÿd’wæÈœÿ 1<V¨¦[p!ÉO•RÞÕ÷À³: ™5ÚSÛý=6ÉG“l��æ€Í NÞ$yr)å³Üp&ÍödÄšäÜ$OòÀ$ãl€) °Šú^÷«Iž�äöY]­{Ã=YýpPJ©éVü`º¡ÿš6›Aºž÷�év¨I³Ø`M÷p2ïÿUI~$Ýbæ‹!ÀYH×ûþH­õU“uÖ}g€uM¦æýÿx’wÇ¢´azQÀŸ(¥¼g½×X·Æ»Ö:(¥Œk­HòÉ$gM2ô€ôóþïLò˜RÊ }¯¼ß|]šïÉ܆2Ù÷ðíIv¦K?4ÿ´¢_g’·Ozä²^ë¬WÞkxe’'æÈÐhÉ0]OüÄ$¯œôÊëÒ¯yÊ05ïÿIþ,ý€¾7~f)åë±ÀšµÖ~„Á9I®Krq ý€qºžüÆ$�LrG’¬åzk݈÷‹¼>É%éþ‚šZ7H×#_’äõ“ÞyMûå5ûâ“á ˵Öç%ynÌû€iýzÏ­µ>oÒC¯Yß¼&S&Cÿkº$ã3IΜ<äî?Ñùß“äáI¾•¤¬ÅT€µjÈ¥”šäµIÎŽyÿp<ý ô³“¼vÒK¯Iÿ¼ê_tjèÿO$ùÉú'ÒOøÉZëO¬ÕT€U�Pk-“¯yF’Ϧ›àî?œX¿+À·’\‘ä®$u2"`U¬vcÞ¯úÿÛI.�Uÿàdô»\šä·×bW€U0ú?ªµ>.ÉÇs¤ù_“…`“©9ÒK?¡”ò�}¯½_|UÒ„ÉÐÿÞïåHÓ¯ù€“3ÝKÿ^ÿÁczî[­áƒÉÝÿ�äû’Œbá?8UÃt=õ÷ÕZÿõäîÿªôî§�"L-üwf’Ï'¹`ò�¹ÿpêÆ“ÿß’äaIöd\�&½_øïW“\ ÿÀéè¼(ɯ®Ö‚€§5 Ö:H·HÁý’|.ɶÉ×4÷V®NÞö'ùî$_OR&aÀŠœn‚P&Cþ÷$ÛsdßB`åJº{{’ÿ}Ò{ŸV¿½âO®µ'óètÛþ•¸û«¥N½=!ɧ2Y„%_ì´FLˆÿ%Ý*…§�F‡•t½ö0Éÿrº‹®¨aŸºûÿ˜$×NȰúúéö�OòɬpÀŠ›öIòð?çH"¬¾~Äýÿ|:£Ny€»ÿ°îN{ÀŠwwÿ`]�ö(€S0u÷ÿ±éVþw÷ÖG? à I>‘SpÊÍû$iøw9²'!°öúàß­dÀI�¨µÒÝñp’Ï$Y<Õ¯¬XßôJòð$_IRJ)'usþTF & Ë“lI2ŠæÖKI׋oIòâI�~Ò}ýI5ðµÖþy;“|9É9§òùÀªèGÜ‘ä!Iv%‡§ëŸÐÉ&ÃÉûé$çæÈ¼`ýôëñ�›ä§'½úðd?ñ„¦îþ—$×%¹"Vÿ€YéoÊ6É#3p_£N¦‰ïçþ?5Ý"š˜�~‘þ‡'yêÉ®p*�ü‹'ßÀÖ0[ãt=ú‹OöN8 Ö:(¥Œk­—¤[üoiò Ìÿ€Ùé{ó}IRJùVßÃßÛ'Ü×€þñ«Ó5ÿËÑüÀ¬•t=úRºž=¹�ÿ¾€Q­u�äySߘ½¾GÞ¤w�èÉ÷ÔZû­ÿž�äQéæœÔÖÀš¦ëÕ•ä ¥”Zk½×¾ýD#ú$¡J`ñ?ØXú^½ïÝïuäþq¨µ–Ir°”äóI.Ÿ|QÛÿÀÆÑ÷ê_Kò°Rʾ¾§?ö‰÷ÖÐ÷J4ÿ°Q Òõì—§ëáû�÷‰ÇÓ� øÉt[ þÓ8]ïþ““÷�;Úÿœþ¿=É—’\#`£ê{öo%yh)eïñ¦¯©ï?öÄhþ`£ë§\’®—ï?v�'«ðœþó ŸðœÉû÷ñT0"°<Ù7ðY“Op÷6¶AºþYµÖᤷ/Ç>aZÿàÓ<$]zp¯{BI×Ã?$]Oßì°c€Á$!xú䉣c?Øp¦{ø§Ozû£zþc€ñd•À™úÀÆ×÷ð?2éíÇÇ{pzû¿ïHr}’³b Ì‹¾‡¿3ɃJ)ßžÞpzÀôög¥K 4ÿ0Jº^þ¬g;ÀÁ1OL’§Äö0�úíŸ2yÿð�ýÁ1OÊäI%îþÀ¼éûù>O?0=ÿÿütóÿψùÿ0oú^þ®tëÜÚ÷üý€þÿ�O×ü›ÿó§_àŒt=~2éùSOH’ï™üßü˜O}Oß÷ø%9ô~ïôƒÀÜé{ú¾Ç'É`2`\kÝ–äÊɃƒc?˜ }Oe­uÛ¤ç/ƒI”ä’Ù°Ê.I×ë'Id*HŒb Ì«éÞþðHÿé��ü¿®ó�««ïíû^¿ rdÀ>p÷æ[ßÛ÷½þxPJÕZ‡I¾ë˜'ó©ïí¿«Ö:,¥Œúùÿ'¹ô˜'ó©ïí/M×ó^ðþI¶¦›# €ùVÒõø[Óõü‡€Oþ?šÁA«¯ïñœ ¾s6Ǭ±ïLŽ™á�kç!É‘à²ÉÿÍÿ€Í¡ïñ/K’A­u1“#€Í¢ïñ/®µ.’\�ä¼cæ[ß㟗ä‚A’ “ì˜ÝñkhG’ é†ÿÓíhl%]¯?Lrñ ÉE“Æ3;$`-ô½þEý`óºx�äüY°¦Îï6¯ IΞõQkêìé)v€Í¥ïõÏ$Ù2Ë#ÖÜ–A’ &ï›Kßë_0È‘=€Íi`í  h€ € h€ € h€ € h€°0ë`ej­³>Ø@JJ™õ1Ü7õ¦©ß°ÞJU‰6´ñ¸¦ÖšZ“R’Á ¤x¥„{¨5�LJϕRÊämÖG´Hý†“£~Ãúl@ãqÍx\³°püµÖÜu×�ŒFc/Ž4­Öî¢úŒ3¶e08þÉ0�“$ƒÁÀù¬)õNŽú ³#Ø jí.†Ã£/¾úÕ[ó¹Ï}+×]÷õ|å+·äÆwe÷î½Ù½{_–—G)¥Äo�•räbçÎí9óÌm¹è¢³ó ]�+¯¼,W\qiò� 29§F£±»pÀªR¿áÔ¨ß0[€;Þ…ÃÇ?~}>ð�ëòWõ…|ñ‹7e×®½‡_ø “A¿>ZÖ�ËËãŒÇ5ËË£ŒÇ5ƒAÉYg-åAº O~òwæéO¿2OzÒC³¸8LâB8}ê7œõfE0C£Ñøð…Ãí·ß�÷¾÷Ú¼ím—ë®ûzî¾û@¶m[̶m‹YXNKû_—ßÑ_ôskíίýûeÿþCÙºu!{Ø%¹êªïÉsŸûÄ\|ñÎ$GŸƒ'Ký†Õ¡~ÃúÌÀxÜýȃ’]»öæ oøPþè�>šn¸-[·.dûö­ËQ §n0( ºsiß¾ƒÙ·ïP.¾øì\}õ÷æÅ/þÁ\rÉ9Irø®À‰¨ß°>ÔoX;€u6�X¾ùÍ“W¿úùÒ—nÊ™g.eÛ¶…à ««¿˜8xp9»wïËe—�›_üÅ΋^ôƒYX¸›œ�ú ³¡~Ãꬣþê†n˯þêÛóþ÷ÿ÷ìØ±5KK[2�í ë ”’áp�åÎ;÷åÉOþμúÕ?�G>ò~“¹…Vަ~Ãì©ß°:ë ›û×½0]sͧòÒ—¾57ß¼;çž{FÆã±;0ý…ÄîÝ{³}û–üöoÿT^ð‚§¾�·À ~ÃÆ£~Ã鬱~à`Pò;¿óþüÖoýi––ºÅ�–—dz>,à$ô×\óɼìeœÁ d4r-P¿a~©ßprLXeýVAøÀuyîs_—�;·›/shaa�[n¹3¯{ÝóòÂþKû Ã&§~Ãæ ~É VÑx\SJÉ7¾q{žúÔßΞ=²eËÐÌ¡RJj­9th”~ðåyÌcîoQ!ؤÔoØ<Ôo81qØ*ªµ¦”äW~å�sóÍwfÛ¶0§jíæ:4Ê/ÿò[ràÀ¡Ã6õ6õNL°JúáEoûÇò¾÷}2çž»Ã>Á0çF£qÎ>{)ÿøõù½ßûË ES›Œú ›�ú ÷΀UÐÿï¼s¾ÿû+7Þ¸+[¶,Ha(¥¼°0ÌG?ú›¹ßýÎM­1”6õ6/õŽÏ€UÐÏ|ã?œ/~ñ¦,-mqñ›D­ÝþÂßþö]ùÝßýóÃs �ù§~Ãæ¥~Ãñpšºyƒ%»wïË“žô¿å¶ÛödqÑÝØŒJ)ùèG3xÀy‚9§~C;Ôo8€ÓÔï/úÞ÷^›n¸5Û¶-ºx€Mhaa�Ûo¿+o~óß$±˜Ì;õÚ ~Ãѧi8솽å-›­[-0›ÔhTsÆÛò®wýCî¾û@†Ã�‹˜cê7´Aý†£ NÃxô¹$q¾ÃœR¿¡ê7MpÆ“]‚þìÏ>•½{d84Ÿ6»Zk®¹æ“³> à4¨ßÐõ:€ÓÐ!úÈG>ŸmÛ „Ín<®Ù¾}kþþï¿2i #„y¤~C[Ôo8B°BÝÖAÉ7¾q{¾ð…›\@@j­Ùºu!_ÿúíùÌg¾•Ä0B˜7ê7´Gý†#+Ô§†ÿôOßÌ®]{³°0œñëa0(Ù¿ÿ`>õ©NÒí3 ÌõÚ¤~CG°Bý‹Æ§?ýõ,/�RL„&ÔÚí'üéO=Iœû0gÔoh“ú À õ/_ùÊÍ RDhÈÂÂ0_ýê­Iº; ÀüP¿¡]ê7V¬Ñøæ7weqÑ�ZQkÍââ07Ýtg\Nq æŠú mR¿¡³0ë˜Wý‹ÆîÝ{'wfs Ák-›ÅiWk×@Üu×þìÝ{ [¶,¤ÖêBæÄF©ß)‰W Z¥~ÃìV ±Ø³gvíÚ7ÙJd6Çrè�±‹´k8,ë‚ÕZ3s÷ݲk×¾ìܹc2¯p}�8u©~�—k¢„Ó¨Ápý0õ:€Ó0�³¼<žÙ G)ɹç.zá¢Y{öŒ2Õu?JI–—»ó˜?¡~/í\P¿iÖ�»Æ©3؆OýÀi)e6©a)Ý�ÿsÏ]Ì_\óÈœuîbê¡õo‚`Öžõc×åó_Ø›¥¥AÆë\Ëguþ§o–õ{t¨f霅<ï-ß�¥s2R¿iÐ[ŸÿùÜúå}Y\¤ªß°®s¬°ãœÅdTM&¤9Ãa±‚70wúÛÏЦ2ô�fE0窩£jMÒüój¼\3:T´Iý†™̹ÃØ g€ùQŽÔpõ€õb\h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€`…j­©uÖGœŠZ»sZ$X¡ÅÅaJ™õQ§¢”îÜ€ VèöÛïÎþý‡’ÄHØàúsôàÁå|ûÛw@“f}óêê«?gž¹”íÛ·ÌúP€“tàÀrvïÞ›»î:�……¡ €¦Vh÷î}¹ýö»3k `^”R²°0Èph$í¬ÐÂÂ0 Ö€yc!@Z%X¡¾�ÐG0Œ€ h€ € h€ € h€ € h€ € h€ € h€ € h€°0ë˜WoyËÏçòË¿#ãqÍ`Pf}8À}¨µ¦”’={öåê«_Ÿ]»îÎÂÂBj­³>4X€zèC/Ìe—�;ëÃNÑ�‡2¢ï 5€Ú·ïPÆãj̉~ÀÞ½5ÿ4I°BƒA9Üø `ã«5)¥8_h–E h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ € h€ €,Ìú8uµ&ÃaÉž=£<ëÇ®Ë`X’:룂õ÷µ¯ïÏÖ­ƒŒÇ³>€ûVk2–¸k”·>ÿó)ê7�Úý­n¤ªß°î§¡”’Á Ìè{'£QÍç¾°×ÅÍÚºu�2›Sp¦ç?pzfzþ–¤Ž“[¿¼o6ß6€áõfE°eòŠuæ™ÛröÙK¹ñÆ]Ù²¥Kö×÷8’íKfqЮYÜù/¥äСåœsÎÙ¹siò±õ?àÔm”ú�$‹ê7 ›Å�õ:€ªµ{!9묥ŒÇuf/ †>Ãú�kvìØ’íÛ·&qód£ÔoCŸaý©ß`ÀO:ïK.Ù™ååQ¯ ЂÁ Y^ç ÏÊÖ­‹“;‡Î˜ê7´Iý†Ž`…úá‚zÐ�ÆDhFÉ¡C£Üÿþç%9ÒLóAý†V©ß�V¬¿`¸òÊûe8Ìdþ °þJé.ñˆû%™ÍÜa`åÔoh“ú À õ =üá—æ¬³–2I¡ãqÍÖ­‹yô£/Obþ ÌõÚ¤~CG°Býö!|àyyðƒ/Èþý‡l)›\)%.çâ‹wæÊ+/Kç=ÌõÚ£~ÀÓ0�3 ò”§|Wöí;è…6¹Á dïÞƒùžïyàÔ âÎ{˜7ê7´Eý†#«à™Ï|T¶n]Ìxl2lvµÖ<ã�Jç<Ì9õÚ¡~CGpúŃžøÄåŠ+.q6±~øàe—�›ù‘G$I†Cç;Ì#õÚ¡~Ãѧi4gaa˜«¯þÞìÝë6«á°dÏžýyÎs›sÎÙ1Ù>ÌùóJý†6¨ßp4ÀiêÄ«®zb.¾xgYU6¡Ñhœ;¶æ/xJ«üS¿¡ ê7MpšJ)�Æ¹à‚³ò³?û¤ìÞ½7ÃápÖ‡¬¢……AvíÚ—g?ûѹâŠK2× ^>až©ß°ù©ßpO΀UPJI­5/~ñåÒKÏÉ�‡ -‚M¤¿{ð+¿òÌY °ŠÔoØÜÔo¸'À* JÆãš /<+/{Ù3²{÷Þ,,øÑÂf°¸8Ìí·ß�¾ðûsÅ—L¶Ó Àf ~Ãæ¥~Ãñ•Z«}0VA­Éx<Îx\ó¬gý_ù‡¸>g�µ”Ñh<ëCVh0(Ù¿ÿP.½ôœ|ä#¿‘3ÏÜ–Râ!l"ê7l>ê7Ü;1÷*é_T‡yík&KK[²¼<òBsîàÁQ^óšŸÉÙg/¥Ö꜆MFý†ÍIý†ã¬¢Á [PèŠ+.Í+_yUî¸c¯}FaN-.së­{òë¿þì<õ©ß�ÑhœáÐK&lFê7lê7œ˜³a• ‡ƒ,/�ó¯þÕ¿ÈK^òC¹å–;³¸hUa˜'ýÅÃ�ÿøãók¿ö¬É¼A/—°™©ß0ÿÔo¸oÖXµ&µŽSJÉÕW¿>ï{ß'sÁgåСѬ ¸‹‹Ã|ûÛwåq�{`®¹æßgÇŽ-IÌ„¨ß0¿Ôo89"±50½ÈÈ›Þôoóô§_éNÌ�þâáÑ�¾<ï|çKrÆ[S«‹h…ú óIý†“'X#ÝÞÂÉÒÒ–¼õ­/Î3Ÿù¨Ü|ó�YXz1‚ ¨6ø¸Ç= ïyÏ/çüóÏÊx\m�Q¿a¾¨ßpjk¨ß_xii1ïxÇKòs?÷¹å–;Skµ lƒAÉ`PróÍ»ó£?úØ\sÍ¿ÏùçŸéâ¦~ÃÆ§~ÃÊX`t?â’R’7¼áCùÿá=9xp9gŸ½”ååQü`6†¹ûîY^åå/f~ã7ž“$.€$ê7lTê7¬œ`�ÔšŒÇÝ6$ÿøWó²—½-×^ûÕìܹ=‹‹ Y^¶À¬—áp�Ñhœ;î¸;ßõ]畯¼*?üÃ�Hÿrh˜/ÐS¿aãP¿áô ÖY¿é�ËyÍkþ<ÿé?ý¹í¶=Ù¹s{�jüJ`õ•R2v{}ïÞ½/;vlÍóŸÿä¼âÏÎ9çì°O0pBê7̆ú «K0ÓÓ®¿þ–¼öµ‘÷¼çÚÜqÇÝÙ±ck¶m[<ü<¿X¹RÊásíàÁåÜu×þìØ±5Ï|æ£òÒ—>-�zÔåIââ8)ê7¬õÖŽ`F¦‡&É—¾tSÞö¶¿Ë»ß}mn¸á¶ÔZ³´´%[·.d0(“½‰ëÔ|Ãjî!¤Û¶+)‡ÿ\J7_w<®9xp”}ûf<®¹ôÒsòœç<&?û³O:êÂa0(† 'Mý†Õ¡~Ãlf¬¿KÐ_HÜyç¾|èCŸËûßÿ©|ìc×ç߸=û÷J)%‹‹ƒ,, 3 2–Ã[yí£eµvãñ8ËËã,/�2×lÙ²�‹/>;�üóŒg<2?ôC�Èù矙¤;ï’X(X1õN�ú ³!Ø Ž½�H’={öç3Ÿùf®»îk¹îºoä«_½5·ÞzgöìÙŸ»ï>�ÑhœRâNœò»[ƒAÉŽ[³cÇÖœþ™¹ÿýÏË•W^–G>òò<â—åÜsw~nwÎÀªQ¿O]ËwŽP¿a6L­õpºy¼9M,gß¾ƒÙµko–—G†>Í¡ZkJ)¹óÎ}¹êª×çÎ;÷faa8“ù¢oyËÏçòË¿cn·Í©µf0dçÎíYZÚ’¥¥Å{<§¿87TXKê÷}Sÿè©ß0; ³>ŽÖ¯tš¹˜è‡ %[·.dëÖ…ìܹ}ÆGÊj8ûìm¹ãŽ»²¸8œÉÝ�‡>ôÂ\vÙ¹ëÿ�×H?7wúœé.ì\8kKý>5êÓÔoX?€ lúb¢×I m¥”lß¾õð£YØ·ïPÆã:÷w@ú…„J9þ�7€õ¤~Ÿ˜úGOý†õ'˜3Ó/”Ì¿YÏÀ9’°[P`-©ßGSÿfCÔ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ X˜õ°2µÖY§©”’RÊL�¡Ö:õ6ÓC9M%3þQœõ[ýcšú ëM°Á�ÇGŠS)É`P6Dáäô�Ç5û÷šéïraa˜RJ†Ãùÿ÷Tk2�Ÿ+ýyâTfAý¾wêÓÔoX_€ h<®�k J’£_k­¹ë®�Æ^çP­5¥”ìÞ½7»wïËÂB™Ù݇]»öf÷î½�ëäßÚ|©µ»¨>ãŒm J†Ã{Îj�ÆI’Á`à|Ö”ú}bê=õf§TcÑ6„Z»‹†c_¿úÕ[ó¹Ï}+×]÷õ|å+·äÆw.œËË£”2»âÉ骹óÎý3zæ™Û2ÌçR ¥¹€Ø¹s{Îp]þ꯾�/~ñ¦ìÚµ÷ð _wWa�áp0yôë›_Ýïs–F£ñœÏGíÎ�ååqÆãšååÑá»9g�µ”=è‚<ùÉß™§?ýÊ<éIÍââ0‰ àô©ß§CýCý†YÌÐh4>|ápûíwç½ï½6o{Ûßåºë¾ž»ï>�mÛ³mÛâdžZ—–ö¿.¿µÍaÖ§ßf) ý_£Ÿ3Xkw~íß(û÷ÊÖ­ yØÃ.ÉUW}OžûÜ'æâ‹w&9ú8Yê÷éSÿHÔo˜À ŒÇÝ�|0(ÙµkoÞð†å�þ裹á†Û²uëB¶oßšá°µ€pꃒÁ ;—öí;˜}ûåâ‹ÏÎÕWo^üâÌ%—œ“$æ€'Eý†õ¡~ÃÚ¬³éÄòÍoþ›¼úÕÈ—¾tSÎ÷uÙ¹s»ù‚0‡¹å–;óº×=//|á¿´Ï0lrê7lê7œ˜`�Ç5¥”|ã·ç©OýíìÙs [¶ ]@À*¥¤ÖšC‡Fùà_žÇ<æþ‚MJý†ÍCý†‡­¢ZkJI~åWþ87ß|g¶m[pñsªÖnð¡C£üò/¿%:üq`sQ¿aóP¿áÄ«¤^ôö·,ï{ß'sî¹;ì sn4çì³—òñ�_Ÿßû½¿Ì`P4°É¨ß°ù¨ßpïLXý�ðÎ;÷çû¿ÿ·rã�»²eË‚¤6�RºáÁ Ã|ô£¿™ûÝïÜÔC aP¿aóR¿áøŒXýÜÁ7¾ñÃùâoÊÒÒ°IÔÚí/üíoß•ßýÝ??<·˜ê7l^ê7Ÿ§©›7X²{÷¾<éIÿ[n»mOÝ=€Í¨”’�~ô7ó€œgA!˜sê7´Cý†#Œ8Mýþ¢ï}ïµ¹á†[³mÛ¢‹Ø„¹ýö»òæ7ÿM‹ Á¼S¿¡ ê7Mpš†Ãn8Ñ[Þò·ÙºuÑ#°I�F5gœ±-ïz×?äî»d8¸ˆ€9¦~CÔo8šà4ŒÇã”RòÉO~-ŸúÔײ}ûV°IÕZ³mÛb®¿þÖ|èCŸKç;Ì)õÚ¡~Ãѧa<Ù%èÏþìSÙ»÷@†Có‰`³«µæšk>9ëÃNƒú íQ¿¡#8 ý¢�|äóÙ¶ÍðAØìÆãšíÛ·æïÿþ+“¦Á0B˜Gê7´Eý†#+Ôm”|ã·ç _¸É4 Öš­[òõ¯ßžÏ|æ[I #„y£~C{Ôo8B°B}jøOÿôÍìÚµ7 ðƒ’ýûæSŸúç$Ý>ÃÀüP¿¡Mê7t+Ô¿h|úÓ_Ïòò(ÅôAhB­Ý~ŸþôדĹsFý†6©ßЬPÿ¢ñ•¯ÜœÁ` E„†,, óÕ¯Þš¤»£ÌõÚ¥~ƒ`Åú�o~sWý¡µÖ,.sÓMwæàÁå·`®¨ßÐ&õ: ³>€yÕ¿hìÞ½wraF·J‰—/Z5‹ó®Ö®�¸ë®ýÙ»÷@¶lYH­Õ…̉�R¿½dвYœvê7t+пXìÙ³?»ví›l%2›c/�fó�a ×ÿî]­5Ãá0wß} »víËÎ�;&ó ×ýP€S´‘ê÷¡‘¹´k8XÿXê7t§a4gyy<³ŽRJ–ÎÚ.¹¤YöîO�Á6>¥$ËËÝùÌŸÙ×ïäÜ[ºú- A{öʨÖuÔoœ–Rf“–R2Zeé¬íyþë^”¥³¶g´<М·¼ô ¹õ†›²¸u˺ã�Õùœ¾ÙÕïîÎÿ¹Û·æ¿¾ô©9wû–W¯%4ç'þã‡ó…wgiËBÆê7¬+ÀëGl?{‡€&+øs¨ð;¶æÐH@{†¥ü3"˜sãåQF“7̇C£zøMù¦5š˜À¼+%eê ØøúaȆ#°žl€ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ ¬P­5µÎú(€SQkwî@‹+´¸8L)³> àT”Ò�»Ð"À Ý~ûÝÙ¿ÿP’ \Ž<¸œoû.£hÒ¬`^]}õïçÌ3—²}û–Y p’XÎîÝ{s×]²°0ÐÀ íÞ½/·ß~wÆc Ì‹RJ ‚ =€ZXfaÁ:0o,@«+Ô7úæ�ñoÐ4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @f}óê-oùù\~ùwd<® ʬ¸µÖ”R²gϾ\}õë³k×ÝYXXH­uÖ‡ëB°B}è…¹ì²sg}À):pàP†ÃAôý´F°BûöÊx\�€9Ñ�Ø»÷ æ€& Vh0(‡l|µ&¥ç+Ͳ 4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ð4@ @Ѐ…Y§®ÖšÁp�{÷ç-/}CÊ Ìú�`&vßtG†‹ ©µÎúPîS­ÉpP²gÿ¡üÄüpƒ’xù¢A_¿ýîl]f¬~úœ†RJW¼g¤Žkn½áæîŠB@ƒ†‹ )e6ÿøg}þ+7Ëó·$Õš/ܸ[ïO³¶. 3£ò­~Ó<À ô Ç™gnËÙg/åÆweË–®_o‹[×ÿ›Â1‹;ÿ¥”:´œsÎ9#;w.M>¶î‡¬ÀF©ß%ÉÒ—`´kwþÕoè¨>+Tk÷BrÖYK�ëÌ^@ }†õ7×ìØ±%Û·oMâæÉF©ß†>ÃúS¿Á"€+6�“$—\²3ËË£ƒm ’ååq.¼ð¬lݺ8¹sèü‡y¡~C›ÔoèV¨îô  2�%ˆÐŒ’C‡F¹ÿýÏKr¤™æƒú ­R¿!¬XÁpå•÷Ëp8˜Éü`ý•Ò]4<â÷K2›µ?€•S¿¡Mê7t+Ô/$ôð‡_š³ÎZÊh$E„ŒÇ5[·.æÑ�¾<‰ùƒ0oÔoh“ú À õÛ‡<ð�çåÁ¾ û÷²¥lr¥”<¸œ‹/Þ™+¯¼,Iœ÷0gÔoh�ú GNÃh4Î`0ÈSžò]Ù·ï Øäƒ’½{æ{¾ç�S+ˆ;ïaÞ¨ßÐõެ‚g>óQÙºu1ã±ÉD°ÙÕZóŒg<*Iœó0çÔoh‡ú Àièzâ”+®¸Ä]ØÄúთ]vn~äG‘$�ï0�Ôoh‡ú Gœ¦Ñhœ……a®¾ú{³w¯ ج†Ã’={öç9ÏylÎ9gÇdû0ç;Ì+õÚ ~Ãѧ©O¯ºê‰¹øâ�9xpdUQØ„F£qvìØš¼à)I¬ óNý†6¨ßp4Ài*¥d4ç‚ ÎÊÏþ쓲{÷Þ ‡ÃY°ŠÙµk_žýìGçŠ+.Éx\3xù„y¦~Ãæ§~Ã=9VA)%µÖ¼øÅ?”K/='2´6‘þîÁ¯üÊ3g}(À*R¿asS¿áž«`0(�k.¼ð¬¼ìeÏÈîÝ{³°àG ›Áââ0·ß~w^øÂïÏW\2Ù>Lƒ›�ú ›—ú ÇWj­öÁXµ&ãñ8ãqͳžõåþáúœuÖRF£ñ¬ X¡Á dÿþC¹ôÒsò‘�üFÎ4à>,.óíoß•Ç=î�¹æšŸ;¶$1oZ ~ÃüR¿áäˆÄÖÀô"#ozÓ¿ÍÓŸ~¥; 0ú‹‡G?úò¼ó�/ÉglM­. ê7Ì'õNž`�t{ 'KK[òÖ·¾8Ï|æ£róÍwfaaèÅ6 ~Øàã÷€¼ç=¿œóÏ?+ãqµe4Fý†ù¢~鬡~ᥥżã/ÉÏýÜä–[îL­Õb$°A %ƒAÉÍ7ïÎ�þècsÍ5ÿ>矦‹h˜ú Ÿú +c €uÐýˆKJIÞð†å?ü‡÷äàÁåœ}öR–—Gñ€ÙXXæî»dyy”—¿ü™ù�ßxN’¸x’¨ß°Q©ß°r€uRk2wÛ�|üã_ÍË^ö¶\{íW³sçö,..dyÙC°^†ÃAF£qî¸ãî|×w]œW¾òªüð?"ýË¡a¾@Oý†�Cý†Ó'Xgý^¤,ç5¯ùóü§ÿôÿå¶ÛödçÎíYXd4ªñ+�ÕWJÉpØíõ½{÷¾ìر5Ïþ“óŠW<;眳Ã>ÁÀ ©ß0ê7¬.À LOºþú[òÚ×þEÞóžksÇwgÇŽ­Ù¶mñðóüz`åJ)‡Ïµƒ—s×]û³cÇÖ<ó™�ÊK_ú´<êQ—'‰‹न߰>ÔoX;€™R˜$_úÒMyÛÛþ.ï~÷µ¹á†ÛRkÍÒÒ–lݺ�Á Lö&®Só «¹‡�nÛ®¤þs)Ý|Ýñ¸æàÁQöí;˜ñ¸æÒKÏÉsžó˜üìÏ>é¨ ‡Á 2œ4õV‡ú ³!˜±þ.A!qç�ûò¡}.ïÿ§ò±�]Ÿo|ãöìß(¥”,.²°0Ì`0ÈpXoUäµ�–ÕÚ]ŒÇã,/�³¼<Êx\³eËB.¾øì<þñÌ3žñÈüÐ="çŸf’î¼Kb¡ `ÅÔo8=ê7̆`ƒ8öB"IöìÙŸÏ|曹åºë¾‘¯~õÖÜzë�Ù³gî¾û@F£qJ‰; 4m0(Ù±ckvìØšóÏ?3÷¿ÿy¹òÊËòÈG^žG<Ⲝ{îŽÃÏíΙâÂX5ê7¬Œú ³!Ø`j­‡ÓÍãÍi:p`9ûöÌ®]{³¼<2ô‰¦ÕZ3 ²sçö,-mÉÒÒâ=žÓ_œ*¬%õNžú ³#ØÀú‹‰~˜ @8±~n®s˜%õN�ú ëG0gºß–„`Z¿��k`£R¿ážÔoXhÀ Ém“? `sé{ýÛIÍòH€5wh�n°y In™¼c l.}¯Ë ÉÁY °æ’Ü:yÇØ\ú^ÿÖA’ݳ<`Íí$¹yÖG¬©›§§›Ó­ƒ$7Îú(€5uã ÉM“w³<`Õõ½þMý€Q’;ÀfQÓõú£LFÜœäî™°VîÎdÀ[’Ü6ù °9ô=þmIn”RåÈB€ØúÿÆRÊ¡~1€oó 0ßúÿɑտ<›cÖØ—“#Àgx ÀÚùbr$øÊäÿÃÙ °Êúÿ+É‘àŸ“H·? u`¾Õt=þ�t=ÿáàÆ$ßœz0¿úÞþ›™ìü7¨µK)£$_8æIÀ|ê{û/”RFµÖá GF|ú˜'ó©ïíû^0˜úà'&ÿ/ëzHÀjë{û¾×¯ƒ$ãÉ;ŸN cÌ«éÞ¾0žp}’oÍàÀ€Õ÷­t½~’ÔA)¥ÖZ¥”ý™JfslÀi:<Ò¿”²Òó×~Àþÿ?ù¿)0Ÿúž¾ïñ‡ÿ3õà?L?Ì�¾§ï{ü:ýÁ~xÀµIîš|Ü(˜/5]OWº?™ôüƒ$™ZàÖ$ÿ}ú ÀÜè{ùÿ^J¹µŸÿŸ=Ô¿ÿó_§K Œ€ùÒ÷ó=yÿpß?8æI™<©Ä:0oézú>¨ÓôúaKrg¬ó¤ŸÿgºÞ>™šÞ8˜ZàÛI>~ì€ ­ïá?^JùöôüÿäžÃüµÖ’ä/&ïó¡ïáÿbÒÛÕóŒ'éÀŸO>q!ltÓ=üŸOzû£FõôÍþg’|9ÝÂØØjºþËézúþc‡LÖX(¥Œ’¼òdëÀÆ6N×ÿ¿”2šôö÷LôOøÓØæA¿ýߟNÞ¿Çhþã5÷ÓÛ~kò£`c§ëÝ¿•ãlÿ×»G05 `oL€�nzøÿÞã ÿOî}xÿÄwÅ4ØÈúáÿïš¼ÜÅüËñ>Xk-“‘KI>ŸäòRl }¯þµ$+¥ìë{úcŸx܆~jÀ¾$ï™ú¢ÀÆÑ÷êï™4ÿÇþŸœøŽ~ÿ o?‰çë¯ïÕûÞý¸Ír/SzµÖ2yÎ'’<*É(Ép8=}�þß“<6I½·»ÿÉ}ßÕ–RÆIþŸÉû÷ú…€uÕ÷èÿϤw?á ûû0(¥Œk­—$ùr’¥É78áçkªïÍ÷%yH)å[}oŸp“æXJùV’L¾Áh5�8e£t=ú&ÍÿðDÍrj û½>]º`1@˜­Aºýõ'û ÷9”²`ÿÜë’\‘.eÀú§ëÑ?›ä‘™¬p¢““hâ'_ JÐ�° ÌF?ÿÿõýâ÷Õü''¹˜ßÔ(€�é<çT>X}£G’‡$Ù•Ü÷Ýÿä$‡ñO�¸#É¥kü-ëk”®'ÿ£I�~Rwÿ“S¸ƒ_k¤Kœä3IOõk+Ö7ú‡’<<ÉW’”ûZý¿wÒ ùM¾à ”òå$ïŒQ°žú»ÿïœô惓mþ“S¼{_k¦[mð±I>»ÀzéWÿB’O¤ NúÆü)5ï“/<(¥üc’?�|¾Q°¶Fézð?�ôä§Ôü'+˜¿?5 à1I®�Q°Öú»ÿ�Oòɬ 8åÆ}jÀ'b¬µé»ÿ§<ô¿·¢ü�€usÚwÿ“6íFÀºX•»ÿÉ G$G�xtºÊÔpzêÔÛ’|*§¬xØþÔ(€O&ykŒ€ÕÔßýë¤÷^qóŸœæÝúZë ]q¿$ŸK²-FÀéêïüïOòÝI¾ž¤”RÆ+ý‚§µpßäJ)_KòÚ«¡¿ûÿÚIÏ=8�æ?Y…;õµÖþŽÿ™I>Ÿä‚ÉCv€S×7ú·$yX’=Ij)¥žÎ=í&}r¥”²;ÉoL¾æi4¬¦ë­cÒk—Ómþ“Uš«?0(¥Œj­›äûÒ W®Æ×€Fô½ôß•RžÔïÀ·Àª Ó?æ@~1GF 'gº—þÅþƒ«Ñü'«8Or÷XJùÇ$¯O—XœÖÐ�qº^úõ¥”œôØ«¶Ðþªn×7µ àI>›ä’™»ß8]?ý­$W$¹+«°ðß´Um̧¼3É/§;x£àÄúà—'=õª,ü7mÕïÌO¦,”RÞ�ä]IÒ-bÜÓ(]ïü®RÊ»'=õª÷Ñ«: Wkí·¼$Ég’œ9yÈT8¢5¿'ÉÃÓM(¥”UM¿& ùä@¥”o&ù¥É÷1Ž6N×3ÿÒ¤‡¬EóŸ¬Ñ€ÞdØÂr­õIž›#û@ëúù¿”R®ê{èµúfkô# ÎIr]’‹cWèý»1É#“Ü‘Q¿&Ö´Ÿx)¥|;É¿‰] 9ü›Iϼ&óþ§­ù�ø©]>�ä5éV6\³! °Á-§ë�_SJùÀZ­ú¬5�Ыµ–taCIòÑ$OŒõhOß ,É“ÓM“—RêZãu ’n=€RʸÖú€$ŸLrÖä!ëЂ~ˆÿ�ISJ¹¡ï•×㛯[ó=iþ‡¥”’¼0G¶\ó”f¬æÈ–/œ4ÿÃõjþ“u¾û>µÀ{’¼:Ýœ‡5Ÿç36J׿º”òžõš÷?mݦô&ë K)˵Ö&ù‘Y6›¾çý‹RÊÓj­ IFë1ïÚºI·@ºáç&ùx’æÈPØ,ú^÷«Iž�äö¬Ã–Ç3“†{òLö:üÉ$û&Y€Í¢ïq÷%ùÉI¼n‹þkfwܧÖøD’çMŽe!ó¯¦ëqIžWJùÄ,æýO›é�ûÉ: ¥”w%yEº9˳<&Xý¼ÿW”RÞ5é}gÚïÎd €cõ?ˆZëë’¼$É¡$‹3>,X‰¾§ýýRÊ/l„æ?Ù8@I7bTký“$?!ó§ïeÿk)åÇj­Ã$ãõ^ñÿx6D�Þ I¶%ù`’'Çö€Ì�¾‡ýh’§%ÙŸ^æ6L�t!@)e\k=/ɇ“<<Ý¢ ÙœXß»~&É”Rnë{Ü×a*H’Zëp2à¢$—ä°qõ=ë I¾¯”rSßÛÎö°Ž6Ó]ŽgÒüK)7%yV’›Òý 7Ôr¤ù¿)ɳ6jóŸlÀ 9*øl’Œ€�gºùÿÁRÊg7jóŸlÐ 9,Ø€Ž×ü/lÔæ?ÙÀ@’”R–…l0÷Öü/Ïø¸NhC‰€ e.›ÿd€ä¸!Àõé~àþ À¦±œ®½>sÖü'pÀ™Ú"ðâ$ÿ-ÉI%Yœí‘°Éõ½çg“üP)åÆ�¼àßñÌÅ€ÞÔî7&yj’¿J÷ 84Û#`ë›ÿ¿JòÔylþ“9 ’£B€[’<#ÉŸ¤ûE,'©³<66•š®×\L×{>£”rË<6ÿÉÉá`PJÙ[Jù“ü~’…$ã8}5]�¹�ä÷K)ÿc)eï¤�»æ?™Ó IJ)ãZë`òÃÿ…$¿žn1†’î—+1N×[“üz)å¦úϹí7çjÀ㩵–$ÃÉNÏMòÿK²#Ý0�…™ó¦ï%ïNò¯K)ÿ¥Öº�dTJ™ëçsôú­j­�Oòö$Ž€“×÷�_Iru)åÚyÚæï¾Ìí€cMšÿ…RʵI¾/ÉÒýâF±.÷®¦ëÒõ’ß·ÙšÿdÉá`XJ¹5ɳ’¼:GÖ˜ËEXS£™ïÿê$Ï*¥Ü:é-7MóŸl¢)Ój­ƒäðB�?•ä?'97¦pDß#ÞžäçK)ïœî'gzdk`SÉ=|H’7%ù9²CÀ¦ýÀI›î ÿ&É J)_Þ,‹ýÝ›MÛ—RêÔº_NòI^“îïÙ%`0úñŽ$�Kò{“‡‡éþá4— Ì¡š®‡ëoäþ^’Ç•RÞQkÖZ-7ÿIã#¦³6À÷¥ ðý“‡—sdÁ@6޾ñï‡ûÿUº»þ—´9×ÿÞhh§L¯ 0yÿß&ùÍt[&ÖØH¦{´¯%ù?J)�t��ëoÇQk$©¥”ZkýŽ$ÿ.É/%9sò”éa%¬¯éžlO’ÿ˜ä5¥”oOnì–V¶ö;€8fZÀƒ“üF’ç§[;ÁB�ëkz�¿q’?Jò[¥”¯$†ûßÀ}˜¤GéÝž˜äåI~bêiF¬�c{®w'yU)åcI·º’‘áþ'&8I“iejDÀ÷&ù•Ü3èÓ(Vîx£®ß�äÕ¥”¿OÏó¯†ûŸ�ê)šéÿ�ÝK`×€•9vUÿäž�ÿQ}'GƒºB÷ü|’«“l�vÿ$?™n�€ÇO=½?„Àfro½Îµéæ÷¿«”òÏýmç·¶klôóUFSR’ŸJò£I8õ)Â`žÝ[OóÕ$ÿ5É;K)Ûpr·?IÆÿµ%XG“éƒcFlOò/“üX’§%¹ÿ1Ÿ¶œ# bø}M¿�_ÍÑsú“䟓|0ÉŸ$ùH)eïáOêîö� ó_?Ê™¤\å˜0`G’ïMòìtaÀwói}’&feºá?ÞÈåÏ¥kú¯Iò÷¥”»b×ô×éÑѬ äŒ�`ŠÀ0ɕ邀NòØ$çóéÓ�ÀôÀj¨Ç¼¯á¿#É'’üeºÆÿÓÇémCügN³¸�L…G� ˜šä¯“üc)å–£¾ÐäN4ýŠÆpƒš„} p�y1µÖ‹’<&É÷LÞ®LrYŽÿ;í·ì  MÇ6ù½é­ùŽ}þ7’|:É?LÞ>YJ¹é¨'MÖ;Ëdj€¦cÒü͉éÑ9ζµÖ¥$Jòˆt#®Hò°$—&Ùv__>݉:­œÄŸ€Ùª'ñçääÖÛŸä›I>Ÿä³Iþ1É?%¹¾”²ï¨o:Ùö<îòÏÍÜœ:&8îÊ™“a7%¹<ÉC“|g’§)pQ’ó’œÛ @ ÆIö$¹-ÉMéîì%É“|)É×’Ütìtää¨;üþ9&Ø$Ž™2�ÜÇÊšµÖ­é€ Ò…—%¹0ɹ“?Ÿ9y|ëä9ý–ç­Ñ_8u·åÈÖá·$90ùØžt þíInžüù¦Ésn+¥¸·/ØïX6y×�þMäÿ­$| ðÍIEND®B`‚trizbort-qt-1.0.1/packaging/trizbort-qt.ico000066400000000000000000000404261526521612000207410ustar00rootroot00000000000000 Áv ~7 µ00 âÍ @@ Q¯€€ G  ÏG*‰PNG  IHDRóÿaˆIDATxœ�“ÍKTQÆï½×Jm -'´,‹¾Ä¾°Š>g%X n‚þ„ ÿ¤Õ¢MaCA.\ôA•Y&)ii6’Nã�­ü˜²æÞsÞê4¶ê�³8‡Ãá}žßyPÕXšD.Ø mµÖj¡ÂÐh°¸‚Ðc¬ª&T5&ªšj„ÿÓâÝO¢ªúðáM|—ÅEär–ÖÖƒD£ÂÐrëv'S?~"@Yi gÏ¢¨ÈU@œ0D»_¾’Ø‘±)–»=ôôŒa­b­’IÍÒ1øŒtã0~ã0ƒO™ð³b­UO‰FW²cw(aÏõ}ŒŒø(ðÍŸcMu)ÛöFQU²=î¼@DÄÈd²¼ïŸcUe9477ÓÔT�_Æf8ý*É¡ï¡!5:½$)Ïuá@ã~=gp(MuõvšNÖâºÑš-u‡y|i�ÚÚ(-u»¨¬Z¹8âŠÇ;Õ÷§UUó(�1ªªz3Þ©éñiýWN!×»”ׂ_Ç]8Ó¿†‡Ó$?Nòæõ(ž§ZX¶ÌCdžDoß(ݽIr õuÕìoؘÀåJÛsâ鈴ûv‘b ?‰ç9¸®Ã\`h¿x�Ó%º¡$íîâgfó“xe¶45P^Q"´ßéæí;RY6G#ÄnUÒ�åw` -¨f³¿Äý:‹ˆ0‘™áDýzޝ`ÔŸ¢·-Eøõ |Ÿú‰’ÏB=pdkUDß>è’ßeÅ,ŸœáعãÔnª fÃjºî÷q¹ÿ3s«ËX[^  ŽãÈß2Y£ ‚ã,IZdþׄ…©” U�£ £jñÿ[ç½1&_ç bø(¯ /b¼IEND®B`‚‰PNG  IHDRàw=øEIDATxœµ–KlTUÇçœ{§óè åU1& tÀ�JÄ@L«†�A‚+Ã#ÑFˆº2McâÂW5†Q·>¼D*`(¡ %JmKK¡ít^�;wîù\Üim;Å _rsÏ=ù¾óÿ^ÿï\Dd¿ˆô‹H "Vî]lõŒ~Ù Däu  ,ÓE)…ÖŠÙ"ÖÚš}­5j¦ú>%"#À|@]cuobŒ)€sçzÈá8!†ï[š›I§”Rˆ€R�/xœýí/l¥º!‚rë6,§¾>:I‰ˆíé¹£^{õcZ[“`AE¡àqåj‚o¾}× AƒÀâ8šO:OðÙÉ£¬lYH)_!šp¸Þ=®ÖM¼ñÖ&*;é¨8€÷hY“â�w›�2`�½{ˆÅœ)o&£«”-ë_yˆç¶/%›+“JF8ýCùnovª”`ŒÂó,¾ï”|ŒQØÀ24”¥½ýkâqS-¬`Åù³ÿðH{„̘GqÌGY(fB»Ùâ@Ø®«q]WK€ óâìØ±ŽDÂ`mØ9®kð‹OMü K]܉w†‡ó f©Lø$’udÇ ”¼ mm+kŒN»Ì©×X18�b¦ÂDÑcx ǃ%n€tzõ©49\ÇÐÓÓÇ¢EIÖ´4OqC)…µ­5ÏnXA×Áëüòû(ã™ šÒ‹Qcóy¾ý1€™Ü™‹Ž»w.ýý™�šöîÄ>v䢼ÿÞ÷ÿKm=Yk‹ˆ`Œ>ÖJMȾ`EÄbD„²Ì©ï„á‡A 3Ö!¹k §Ø@°ÖNéÚ@ª=9€1cÂu4aá¢$ZkªDŸi䄊 óâÔ'êp=Å‘9¬Ž¾ÀÐÀ(nÄáò…^v§íÅÕ<³®iڨ߷†Æ9|üÎ_èåÆßwXðÕ)‚JÀ¶ÍOÑø@jJÂQ!=Wn²}Ï—¬Þ´–Àó©OEÉS¸t�#ßíÃqÃ@'GŇáê�]´­}˜²WAkÅé‹}4mn¡ãí-ÓGE�W*³ìñ¥lnßÈD¦Œ2 Ï«pòƒa\Ç ªm§u˜ ìl]ÅÆ…ñ"4$X¾ ί•Ú^-²¢\*3~»H)[D;š ÆØµë‘X±!!£é¾ÔÏ“­Mø£²�ŠÑ¼‡ŽÆï^äH�K"G‹`\CÉóYܘ¢£c+u±"aŠ\×ÐùéÏ8TpS1“¨#s™}ÛLJ©›½wø³«/_¸†bÁ#—-²rÕ’Ãd}”®s×X²0A.W"™ŒÒ}õ忨ìóÃq½<½„­ë—qû§3¸ŽÁ*� Ÿ�ÛžF±ÿñCkÅ–—žàPÿ_ôåÂŽÉø”ç³÷å5ÕZM9¤îû•©�Õ–ÙÏœÖVjô‚ÀNºzæ�0Q÷ñ·å_ 6Д»+‘IEND®B`‚‰PNG  IHDR szzôßIDATxœÅ—]ˆ]WÇkŸ}î÷Ì܌󬋊†j4±–’hŸÒ’èƒô¥~‹`kAÛZôaHúÚ…@EA*HKúÐÔP¬Á”V�h£’jÈt’¹3wî×9gŸ½|8÷ܹ3sïd,]°¹çž³×^ÿ½>þkoTµ¢ª§Uõºªz}ÿÄ÷mœVÕ }ã5U}ñ}4:N^TÕš¨êà@â=VD…"2òõQÕ1_„!uO‹ª®ÓÞ{1ÆŒµ¢ª{±GÑþX‚Ýlöˆ"·Å�ªR,Z&'K»¯¨ÊêÍ6b$[@À{åC3Õ±àmšz‚Àpþü ¿¦^/âý¦+��‹‹ßàĉ»ÈçgF³9I’rêÔ/øû•·©T }c„N'æà'ð«sߣP°®!,6wÀÅ‹ÿá¾/[ž|êÓÄíkç”BµÈÏŸú'^ºÊñãqÎ çaY[mó·+×øÚ3er¶ˆ‹=¡ÐZMxö‘k¬7:ÌíŸìçÉ&›? !¥B‘éé2LX§–¨O…Ö°Ö`­ÙáÆ¹ùIjµ3¦æK[˜@¨V ãC0C—*ª{¬Ï<`­£P,ðÇç—@#œó乪š¹³ÓJh·{�‚‹R\¢ *¸D·„t,€í±ÙþÞ¥ž8vÄqºe7b Š’,,ý÷²mì&"™»D a˜… @,Q/æÄ‰ÃüäÇ÷�^Eáð½o�(¶`PõÉB„Â.Õ½ Ž¢nÌÚZ�¤a­ALÑÐl&8iáœÇ9?ȃ< Wo¶h·"Ú�˜ 4h >õ+´1�v<–¤lîΣG?ÆÂÂ_xõµ7PUÔ+íNL­Vdm-bqñN¬5ˆ°­ •út…ƒŸ:Ày›r¥@«Õ£R)`ƒ€v;â3Ÿ8ÀÔ¾Jþ6o “s£ÑÑë××tyyCß|ó†:ô„.-½«�Fç¶Äî\ªË7šú¯·nè>ÿ„^y㿺²¼¡7®¯©séX½-UP¯—©×ËT*–rÙrÇSÔëe¼÷³³s an~‚J5¤P²ìÿð$ÓÓµ±ów䀈d®ïÇ5ŽÞ+qìöÔ¼‘þü!½L׌­°me(ƒßíÏã;]®;Z/ÿv[¨*iºé�4Í(7M}Ÿ|voÉiê¿ÃzªŠ12HܱDk7 ÌÌL,3³“#éwÇB6è s3{ÓËw¼rsƒsg_¦Ûê†N'æÝkË<þ£ß23?Å·OennrG>äÿÏŸ_âåKÿÆ¥ž«ïÜâ‡�?ËÄD‰^/áØ—>ÎÜ52—$IœZpfñ9NŸ½Ä‘ûÑmu1�¡V-áT¹üÂë|ÿ[_äÑÇNn#¢,¾ëÍ.÷ÞýSŽ}¤ÎôT™r¥ÈÆF#ÐlE\xg•Wþú$ûê• !è¶z9þ9¾ù³¯³¾’`C;¦÷¢N„ˆ†Á&úþ: Ô'JœyøÊõ DŒ�Яw¹ïé D±‚üÁB·Ýe}%¡y³‰ >õ�ÐëDt�Òh´GRñ­F—zÞ[ë2�»,!Ck¸µÞ%õãËx¨ ò†d#Û�09Uáw¿y‰W._Ã%ÃÝ0[8ŠSz­ˆbØ~ó±FviD[ ÙŽ|êñý’ò©G6Ö;<øàû*qœª%'™Õµ6ßyè¢$%Upýþ/^Iw9 lõ@ª”«%¦fC`r‚©Ù�b¥Hµê™��¹H±bmÀ\½L©^¦ÔÖ0'˜ñD6Pª–xõ÷—06 Ûêد\+sù…×¹û¡ÃÙ©iDà•Æz—GÏ]b_­4ð€5Âz+bµÙ¥Ú‘Ä{¯"Â{+œýåŸè¶z#äGGï•RµÈÃßý óóãyà¹çÿÁÅ?_¥P´ƒC«Ä‘ãØ=wròägGó@~1ao'¨ÿ— .&8ïqI’j~êÙ>n׌rî5ò>1dÜ™mý€/§¤ðzþ?äš=0; ÄóIEND®B`‚‰PNG  IHDR00Wù‡©IDATxœíšŒWÇ?÷Ǽ™÷v—ÅMŠ6•ÆR‹%±ü¨ÐP+“&ÔRIÁác¢6mcÐÄ4mÒ¿HEEÒÄ?Œ�&–¶H4jABc P Öe³HY ÈO—îî¼_3÷øÇ̼Ý}ïí{³@ŒßäæeæÞ9÷œsï9÷œs"¢Óߥ"òªˆ|("Nn8IxzUD–Næ1éïs"ÞB&ó"‘ç&óŽˆ<;i@$·—ö38IxËð,€‘�€J[²4·/ i{HÏ6í¼Ý™‡ -ðŒ‘ À]éKÇ‘|”ŒQ(¥rÏìœà\>âZ+´ž–vFä’G²uîDˆ%e^DPJ±wï Þ~{ϳÈ4KáœÐ×çóÔS3{v©ñí´³¤ýCCxó�C‰þzÌ &Æ?þÕÅ,Xpw7ÚÊf i­ükÖlE\� 0ÄNÚ.�µš³çF9vìÛ¶­Ç9Á˜ö“$:PTkÖ¿Â�Ã'é(Çí•cŒâÚ¿+¼þÆ;ìÙó‚ ÐQ›I Š F­óÇß/ã¡ýD•c¦~àØÀ²nÝaŽ¿Ði¯Éä•J•³§®ñØ÷ïcÕ÷æ^­£›„v±PðØû³³ìÿÅEÂr•b±ÐÑ&í”›x©¤èé5à 4kÖ XƒV‚1šÄè§×PÖ'F)°‚߯‰"ÝV¿_£<Áè|bŠ™¤Q$ˆq]0®‰'h#8ÆhŒÉçyûúP ‰!®'´¥™v<ñ>¯'´í^*5µµë³V3r1äСS8×}ÆÃ õzŒÒÝiÏmè¡X,ðÞ?Îðè£/EÝÕe¬ÆctníæÁu  (—k<ð¹{زe-Qä:®€Öа\eÓú_ÇnÆZî„¶ˆLmíú¢È10ÐË#�Ü—k"'.9[&G2Í+ÑîÝLÈ4“…Öª/d\2Pkˆc—Û �Ž… ‚Ò ­JZ³‡SIßLlaŠÉV€j Êe‡«¸Æ$’Æ1J+Làšìéî(¥ˆEÀ)êãŽ(P‚sc J%^¨8pŠØÉ´‘@‹ÙäsæôS*YÖ~í ÅÀàD'~1ðŠFFB´Vœå[La²=’3 |î¾w;· ò·_Ÿ!ŽA±@1ð¹6\ª±Šk—+,º.¥¢ßÕà•¤bfLìÞ}Œ={N4b!çÏ3ìß?ľ}ClÚ´ ß÷èí-°aÃJzsÇBïŸ8ÏŽßDxžfß¾!öí?ɦ�_"(�Ô Ž'Ö,eáÂ{ºÒ&o:´}û.™3çÛânb®¶}û.¹ëi¶x¡æ| ŠÖjÆÇkˆÀ•+c�ÔZ}]ùÀdš4ÑÌèuÉh ]h`­n3FcmgÃ�“™ºi4g4ú6Äÿ¸Õ¸ãhë…&Ræ9$uMÙ³s­u.O1A+©LG31âl´ uwývõB…B’Sú¾‡ÒŠÞ¾ õ&¦ùÓ®ÈÊh¾‡Ñо^?UÄdeäSÌ”“Ø9aëÖ¿ðÖŸÞ£à™ä�$.ïƒS—¾Ìòó1FSì ØüÃÕ,ZôÉFQ`:dý;w¾ËöWþŠŒ1œúàÃÃWXþ…yxžA$ ✰ñ›_dõãKºÒV""Ùv8|ä4¯xž{Î¥Øßƒ‹\£üQð-~Á26VÁz–ãyìËðÚŽï4¶S;d¾= k|þÁáFCîŸ;@µø–BÁ2>^EDÏ3 ž! <þþî ôõyªÉÃøxßjÖn~’y+>Ke´ŒÒšD)éEQšUäçßx™±ka¢…‡OT�«F<ý•E<ýää£J¢mH…'¨Y?}ý/üá(õZÔ•îÐ:1°JX¥å�Bê%Û˜¨Ó„IŸ"Žb†¯†¼süaXMн“‹P*ùœ¾2NÅù«Z'K;ÞÇ™¿àüöÇ¿I3$hw ¡Tj1_þ €Ž÷IeZèéõYµò3lÛq�_î~?ÙŠ-”]«Ôy|õbúf¥ÿi•#éS‡œ??ÂÑ£ç°ÖÐ:(Î9J%ŸåËçuÕЈq°Æ�ÿL_´ÕMãöfÙ²OÓÓãOÄGÓ�žrÉ'"j¦9é-Bã’Ï»€u@¬”2YTšyï³Î€¶î|Ááìºó/º•RG€Í$i fÆ5âÿ „„7MÂëf¥Ô‘ÿ™?{ܱ·ù�³×ÀÜIEND®B`‚‰PNG  IHDR@@ªiqÞIDATxœí›=ŒIǯºç£gv×û¶gYçÈÑ%ˆ» 8t«“Aæ Á±3Žè$6Å„Žï$$ à6°‘1ÉIçä$#qxmã;ã�éé™îé~ý1;Ó3ÓÛëÕzï/•vÔ[Uÿzoª^½®ÿ”�@U- “Ï?~|¸ XœO„À!ðwàw"ò'·•ôAò÷ºªÞÓ×÷Tõú¨Í¢‰7TõûÀ�5 J|#I9ÏФàxOD%ªúMàÀ 0ì³é+@j[ø–ˆ<4ªj€»¼þÆClÛ€ØÖ»ªjDUo&ç5Ø-‹ÔÖàÃ5r‘ À-QÕÇÄ[�2ðTÓ:Ë@�Bæ+àNm=U ‰£c†(RŒ)fÉIÚžw$ª:ÕÕAÒéôî0Š”f³Fµzòšr‹‘ùA@OÀ=Ö"õàÁÁ?ùàƒ?àº"é”̃€ªâ8ö÷Ì�o,ýmLr{Ýa!ûQ V· qg3 5²ßxóÍ_óÙgOÙÙYA£h¡A “C—ÝkÛÉëZéy¿ªÒn÷¨×«¨F 9AU1´ÛÞ³¦,”æUÅ2†n×goïN¡8 ª´�z8V¡é\§òæçy~!bŸ�Ó "„QD£Qåàà}¶¶V‰¢Å—€1†—/»ì½s‡Þ _Ê;Å"(}ˆ—/¯±¾Þ\º­ãTb‡½ÂI�ëÕaÉ«ÃDßdÛàâAPðýÁÈÑ2³áuæ ×¶-ˆ%HNR% Ø‚m+Œn�Ël…i"qvilÉOi“DHìâëå˜ÒmHDh·CLE¦&BÃí*@·fÆÝÊÒDȈàwB¬Šd‰PÚåØ8ø�âÜ#ˆ½]¯Wq›G�^ðöÞ'qz;:’Ï–‰�Â(JZ ÿþ¼C«µI£QÏú\ 1·ãT©ÕmÿëýôS¢'[V’¢‡š¥Ü)ŒÏÿã²S€;;I_iëõø•vww‹Žkp] ·kÅ“Ï�ŽáÉSŸÃ§>�Ž¡Ûµè¸†Vk“ýý›¬¬Ôˆ¢œvÒüIîol�gaz6¦g#ž¡ó, ó,ÏÄÏ’ÿ™ž�z;E¹gˆxžÏÑ‘‡1†4ʨ‚1‚ëöÙÛ»ƒçùü’ííAÄêZ�F£¶sRn1‚‘!_×ó9ø8æn±BE¬®:4Õ¥¹¦Á(R§ŠãLïðÒ%‘xÚ^¾|‰õõÆXÛ“‰Mr§|(\¹2Î7ÙvYî©0F¦“iÛÕä–wã'¹á›l»,fnƒ³¶¯É·¼"[Þ<Ìêÿ4øÌü*¯7¾rÀYà¬ñ•Îzg� Ûà4�rt}6<.W�Ï7ŠbÜ33Á8©ï1Ýw+•áém¥2~\ž8* ñMo»8ff‚ÓÊôèÊu{ñ7 ðå—.–e BVVê'GǸ¡£ëöãÌ�„Ϙ±ã¶H‹‹£c/CÇÊ~–ó§^b¦Ýö@auÍÉÒ×Z½Âþþ{¥ˆ£®çgó/Rhy¬®Õ1bPtì ݩم¸sÅÑ­+ëÉ‘×Ä�€¶ÇÏA�Æ„_<{ɵݭRÄÑÖ×VãY–PÇ| �µž<ï°{myîcâh,PöÙ¾²Î­»·qV„axlÍe'G€±,úÝÞ¾‹×íãºýBâhÏóq=ŸÖ×WùË/¾ÇF£JÅßôèIÕ°ØFèô¼{ç>®ç/Í=U�JpÖ8k ÂÁqŒŽÂXc"UL�r”Û$âèF£Êf³Jæë’¶%T,“ŤRÄÑ¡@&%ϪJ4³¶eˆ£A¨YÉs€&uÓ±,‹ü°™�Ôæ �2¡`–!ŽŽ £Ë³ËâÅÑÊÒâèQ»wžÅQ°,C§ë³·÷›ÂâèÑQ�Í•Ú+sDù3ð¼ �" ¯HLQ¢8 aÑhÔøøà}¶ ˆ£G/»ü`ï?(íxmNA…+—gŸÜæ¡áTXä§�e"ßi8W ¯S†8šu;G˜�Wg¦: (-ƒ±¬$­ÌO„Ä2#íO&ަžmIœÏÍI„l«dq4U€ünË6 e‚~·Wš8*FèôT,³@&í^2¾%’¯c ”U?ú/Þ¾;"PæÃˆðüð;­�yl9q´îT©×*ðVvoX$Ì Ô z}þÿÈY±ÍVŠIEND®B`‚‰PNG  IHDR€€Ã>aË IDATxœí�MŒÅǯºg¿Ì®½ŽÁ‰‡p à .bAB° —D‰Ä)R�  ¤pA8 @¹ %V$œŒ‹l�@0661+Ãz?¦·?^Õ½žÝ�Ù�w÷ôL×OjíÎnwW½yÿ®ª®¯'lƒªz"·|¾ ¸¸¸Ø\Èv÷r䊋À7À§Àqà™[;aƒïÚÑÑiª*€ˆH¢ªððÖñS—ŸG,c…ð ð†ˆÄªjmwA[¨ª‘$ýýp8ØrJÒr½{ò‹¦€iùû‡À9ë}ÚÊ&çe'ªêð7à‘ô_ÙÅfã5ŽJ±ÑO/�‰Èr;¬@‹ó¼Ž-î�㇓V¿~'"g7Š`Mª*"¢©ó�7à—˜iGþd>œ¥"�¬M °Öà3À8ðp��dÙ‘7™/ßî ÍŠu“¾.<ƒu~„sþ(ÑÀúô6à™Ô×@²wEU½øOz¢‡kÝ� ÄØêàW"ò¦ªz’¾'úÀÀÍØÆƒkð�&™o?n¢¬Ex?ÎùuÀ`}|3p¿ˆ$&-f}‡‚ctÉüü°ªQÕÀ�ÉÁæËQ2+À/ p'Öù›º #K‚õù�ÛÛ®ø¯™¯o7À é×ø«™¯oU½ˆÏwÔ�EQÕ„;}T¡øC�ŠuE� Ý*ªÚµEIbO5¦œ–�^UòQfz]�ô%‰®ehq1`u5D ’ª*4ÓÓ›Ò.›Q·»«@U>ÿüO=u”¹¹¯ˆ¢d-Ó¹f(µ×aÿþ«yüñß°ÿÕky(“vv‡Q‚0:vo+€L…_|ñ-÷Þû,§NÍ333Yx=¥ª,-ìÙ3ÍÑ£�ràÀ5¥–[Ù]T+À «\»»~õ{úéqêÔ<ûöí¢ÑðÒK‹; Ÿ«®šáüùEž|òŸd:ÍûÉÛŽ�v1xs)ßî-ÛªVyKK}ô33A(³³>¦€§A°÷\Xˆ ‚ˆ™™ NœøšùùEöì™N¿�bK�­ìžØé“zjx°˜”jwW�À ˆˆ¢!Ьó�ýû³³ âHs«TÁxBÐŒ9tï'œ>àûE ++«ù$ҙ݈�ÄÊÄŒÇÿþs&wù$‘æç1ÊkøŒ…³«à•cwWØè`ؽ»ÁÌlâ|¿"Ó·DÒ'D6——ƒä|¿¼Éò—w>e·ª9N5Ç  æ ͪÕKGiÙŸÕmŒæÅÐÀóLÚP*¾¥”¥áû^ái šÊ @ÄöË//ATÊ P–ÆâbÓþ^hjƒ¥ÒPUŒVW#~ž±1Ÿò^”…0ŒYXXÁó I…û&.‡J  •o¿ý¡”q€KØ´�‚úþ+ÂÐÀó<2§Ïh>ííŒ�û¥Ï RÕtpt©´D„D•ññ¯¿þöí›%Žã�vfΠ‡?Ï…ï—02š]&•¤D"\sÍnöîÝYjÚ;wNbŒ”> ¥L*/€ŒÕUû Xæk M³Ð¤ÎÐ@DÖ_VgÐ æ ”ÍhVlŽ®q¨9N5§ï6@£sšî]Q醗¶U¾g—ô-ÏĤ祥�‘tR¨çU·ñ%^z$9NáÒK÷-›®Ðî‰l6c&VLþ“B=!X‰7¥9ˆ±ùvIFM%l&$!ùN ÷ì½7–EÛÝ•ÆÇ}|ß  ¾/,,Äúõ'Õ3« ç¿ i4„$Q<Ï01Qþ¾•™Ý(_–b^ûýgöI-À/ª°4aJ´{KˆØŒìØ1Î�7^Í—_~ÄÞ½3AÄéÓA_ nTt§wíFC󙟿È-·ü´´UAYžÚÙ½D,œ[ÝÒùºáŸÒK~Å ­L»»^zâÄiî¹çYæç/2=ÝÿâPc.}% h²ñ+Kÿ§°´055ÆÑ£�rðàµYºÑî­ü l^ÓŸt°¯ÓõeÛÝÓò𹹯9räœôfw¯öu¢L»{êÈ2Ô뫉¦ +³½V¶å«Ê€Ìvv÷k_'Ê´»¯Ž ~2×iJwÙ£|—ÃVùVûÜX@Íq¨9N5Ç  æ8Ô'€šãPsœjŽ@Íq¨9N5Ç  æ8Ô'€šãPsœjŽ@Íq¨9N5§¯9�½FÐÜjïÝlÂäæÿ OäÐþìëD¹v÷$€õ-»ÏefP£ámšéû^ÇY°ÕŒÚnâ§ýÙ«}Ý¥W,¥Dµ+^„¥¥UÂ0N¿0E.\XbÇŽq¢hýÂ�aŠÚ«}�ªÈ¡½FÐÌ6}¾xqe혞^¿[Õ#‡vŠ˜Ú­}�ÊÈ¡½®�Àó½u‹Cã(i³¢6[$Ù¬däж;Ö¦‹C<ÏÐj`'tµ¸7=gh"‡¼#BÅÄé‘D1Fh{^£a*94ͰýÙzx‚!LÂ8=’ít~›ë12,‘Cabz²Ð¢)XnV2rèìÔ˜µ»ˆ "€‹Í°º‘CmÍ„‰éIþô⟙œ™"‰âü6ËI·ˆ‰‚�Wy‰…ÿ}žxäP!JvM�sô¯¿dvjœ0é}y|'ìÖHB3Œ¹ï¹w9saßH•#‡ “3SLíÜAÜEë¶kR„ÍÙPç ™Ÿ_‘DTÕ`»„?nÆ6\u0šd¾ý¸ˆ "² <ÖrR…Gä}¢Xß<–ú\ŒˆÄªê‰È›À‹ØÒÀµF�ëÛEäÍÔç±ÝÕVU°EÃ8ðpå�Á:Š óå{À]@$"¢@DÒ�ªdx˜K/íuQõ Âúrx0õq’úüRcODTU�ˆœa;ˆ|l½1šk£G›Ìo>Ö—‡Dälêãµ6ÞºÖ¾ˆ$-"¸x!=Çà„0,d~Êüöpw‹ó×ù°í{뉪z8Ü�Hv½ë;,­Š[è�#"r Öû´•ŽÎK†’– ððp;0•Sæù²Œ-î_ÞHßð  ­Å~+Û>½ÙëBË盀;°B¸؇�OàJ‚rQ`;œÿ)ÖñïˆÈÜÚ |׎ÿëi“þx=ª�IEND®B`‚‰PNG  IHDR\r¨f–IDATxœíÝoŒ\u½Çñ÷÷œ™ÝîÒnÛµÛF©\«4j(hÒ¢5xQ1‚< jñ6×+†kxPŒ÷Ñ}D�ü“½ÑD“›ÒPx`ôB¢¹”ˆ^µhBø òWIÿZh»ÝÌžó»ÎùíNw§ÛÙí™Ù=ç÷y%“¶ÓÙ™3û�ïçüÎïœ9Çèçœ1�šYÚtÿ*à2`3° ¸¸X Q'–Gd‰I�ãÀàoÀkÀ‹ÀAà%3;é蜋Èú"13Wô‚X‘O–7~dfIÓ}�k�/“5þú"_S¤bÞ! ‚ßO˜Ù_ý8çüJµ° ($f6¾s®¸ø:p PŸñ#> ¬i #‘%Î5ýéÿÏxLø=°ø•™�B±ApÁM看›øp'𱦇Mæ¯ñš"æÈ6Pkºÿuà~à—fvÎî½…Zp3úí|3›tÎÕ€o»€ ùCü‚©éEƇL�Þ~ìnê½Ï,¨1�s‘ŸÜsÎ}ø°%ÿïI²¦×„žHqÒüæGÏß5³?ÀÙ=9ó?ìÈ“ç^à?òçQã‹t^s8à‡Àæ£�yoÌ+œsµü…>ü7pÓ“j|‘îI™žDÿ3°ÓÌ^÷=Úî“´MÍ-°XC¶Ö¯Íý“"ÒA¾ÿl7³'æm­µ�sõ¼ù·“íŸ\C6ɧæY\5²^\üÎ9·=ïÕ™»Þ[:ï iÍ¿x ùE–¢æM‚f¶¯�‘ÀœТùý,£š_déiî϶BàœÐ4ÛÿEà‰¦ÿRó‹,]Í»¯5³'çÚ;Ð2ü>EçÜGÈŽK^�†ý"eá7Þ6›Ù[ç:N`VCçGøY>‰°ÌŸPÍ/RYÏûò^¶¼·g=p&?\øðY²Ý 3¿¤ "K[LÖ»Ÿ¾—÷ô¬>>+š¶û?Gvp�ÿ!Ë/R>Žé¾ÊÌÌœ˜jl?ôÏü,pEÓ‹H9ù~žìû: àü—‡š7â|’à[dͯ¡¿HùùM�+€oå=>Õ×Sk€~àUàChÖ_¤*ü^�CÀÇ�Q3s¾Áã|HðMàb4ë/R%~¯ÀÅÀ7ó^�áì]ËÈÖþëÑÚ_¤jü(à²QÀ8dMî×þ7Fk‘*ò£€7ùQ€¿àߘ>9¡ˆT“#ëu€ÔO~x…é¯÷j¿¿Hõøü$ð 3{#Êç¾Dvêî5¿HUY�×�/9ç,Ê·®oz€ˆT—ïñëÍÌEιÀ•ù�šü©6ßãW:çVDdG­#Û>Ð@¤ÚŒ¬××WDd×ëkÞ "ÕæwõoŽÈ®Ò+"áÙóhø/ßë#àŸfÜ)"Õæ{ýŸ"`ùb.‰ˆ,šåæœKÐî?‘¥º˜§H¸"5¿HÀºzm?çªñeC3Í—¶Cõ^úÌu¸JiêpÎÇÕl$IŠDD�Eõ.—Ž@’¤g}NŸ#Me Tç²µÁÊ•}S÷eïÇJûžŠ¤z—OGÀ?£¼ðÂ;<üðS<ýôë9rŠ$É>eúe6ƒ¡¡lÞ¼�[o½’­[/²án•‡ŠsQ½Ë«ðÈž-{Ê»ïþ5<°ŸÑÑ÷éí­Q¯ûkŒ”ìÓ0ÃädÊÄDƒz=æ¶Û®â¾û¾JOe>ó¡z—»Þ…€sÓ?·ßþ öìù k×Çiš–n-p.Ù¶ á;všë®»ŒG¹“eËê˜U{Ò¨™ê]þziêˆ"ãž{~Ã=÷üš‹/¤Ñ˜¬Ì¡•žž‡ŸdçÎÏó³ŸýëÔï ªwùë]Xø_ÄË/âê«ï¥··^™Ý@çS«Eœ81£�ÞÅ5×|rÖdX©ÞÕ¨waKí‹ÿÐC� Ž[§b6d*ï­õ{ÏþoÏž?åï±¼k„vµ[o–@ÍTïs+ì@ ¿ôÀ�×èí­“¦­×�F¹×q<{Pš:úúz8xðmÆÆÞ§¯¯Dsi·Þé¤+õ`Û¬ïÉV©Þ…€óÃÃã>|Šz=n¹hƒƒõRïCN¦vmyÎ9êõ˜wß=Ñ#§Ø°ahj-QEó©wߪZ©gRÜŒp«R½ =8IÒ–k³lÍ?8X绂�Á:®QÎD¾rÓó¼òê(}}錓¨¥©;çš°ŠæªwÒpô­®ñ/~’¾Õ5’’Ö{ïÎW8þÚõ¾WÁzç+°\´º‰›5´*ƒ8¶JÏrÏG;õî[U£°¼`çšÛ¨ˆ®~²‘€K\iGjþùI'IÕ6Ê<ÑŽ®ÀÔì곬R!m̪Ëâ)çÎK)„@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜ `!}]8¤÷ZU €‚•õì°óá¿ÖÇ‘¾â[rÕÿ´v‘sŽ‘‘ñÅ^ŒŽó§B'I4 (³®Ÿ¤êvìØÍêÕ-öbtÅðð8ÃÃcÔjq0ר@�ÌŒcÇNsèÐÉÅ^”®ˆã(¿þŸ”• `õz�žžÅ^Šîh¾6 ”“ `Î9ÍŽKihP$` ‘€)D¦ ˜& öàƒwpÉ% MQTÍÃä¦/:Æöí»9yr„Z­¦=%¤(ØÆ�ëX¿~p±£+&&Äq¤½%¦(ØØXcꪱUŒŽ¾¯æ/9@Á¢È¦¿º�õXÕ÷MŠL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"+ô„ f†µ¸\¬sÇÆðpÂWnzž(6(é™dþö÷qz{£© d6ËÞ÷—i±ÌUï(6&Î$ìÝù VâzŸ:4AÜá*ZïBÀV¬XÆÐÐrŽ9IOÏÙ×�7ƒ$q¼üêhi? ½½³/‰mf4“ °ví@~ß",\—´So \ Ç_[œ…,HÜSíz6H’”8ŽØ²eÏ<ó&Ë—÷’¦gwºô÷•{«£Õš?ŠŒññ—_¾žåË—Uú|€^;õ¨—¼Þ­ÖüUªwáÕÙ¾}+õz|ΓE¦i¹o-‰‘Ñh$ìØñ9 ¬ fž¯Þ.-÷­•*Õ»°ˆãˆ4ulÙ²��;?ÏÑ£§èé©þ9G{zj?~šnø7ÞøiÒÔÇå^ëµCõ®F½Í_þʸãã ¶m»Ÿýû_bݺœ#VãʹþŒ¸Qd?>̦Mëyì±ï°fÍ Àµœ«"Õ»üõ.4`úœñ##ìÚµ—}ûžÂ –-ë¡V‹1ó§•.òU»Ç9H’„ññ�FÂõ×_ÁîÝ;Y·ne©·Jõ.éË0ý¡xüñÙ»÷Ï=÷6'Nœ!MÝÔ‡¢š—¥fƪUýlÚ´žmÛ>ÃÍ7o¨Ä‡a¡:Yï¢ë7_U¯wG¦‡‡þ—4::ÁÑ£§I’´ãõš±±÷Ù±ã¿8vìõzëk×-äZ~CC+èË_ Ê> ,B‘õîtýæ«ÊõîØ¬�?H"I²©Ôþþ^6lêÔË�ÓààE:ôÞìýÔ¹…^Ë/MÎù  j|.D§êÝ©úÍWUëÝñi[?Cê×Ýv¾ÎB®ågæ×tÕù ¥èzw¢~óUåzwm¿ÍR=l2„kù-†nÕ[õ»0åÝ�)"L 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"SˆL 0€HÀ"Sˆ¬ÖÍsÎuóåÚâœkºµ÷3fÖÙ…ªˆnÔ{!õ›¯*×»ã�¦Yqâ8Z”_dÍýšµZŒ™Çó[¶$I1ƒ(Ò ªYÑõîTý櫪õîh$IJG@VœÓ§ÇHSG§sÀ90ƒ±±'OŽÇvεÃÉ“£œ:5Jšºó~زç5V®ìËßùû±Ž¿§2(ªÞ�ªß|…Pos§ùg4ƒ^x‡‡~ЧŸ~�#GN‘$Ù¢[iš2<<>çcV¬XÖVªûe6ƒ¡¡lÞ¼�[o½’­[/²¡h•‡ŠséT½‹¬ß|…Rï {¶ì)ï¾û×<ðÀ~FGß§··F½“­:ÝýÙkdCù?I’Î{[ur2eb¢A½sÛmWqß}_¥¿¿§2ŠùèL½;[¿ùªr½ ç¦'~n¿ýìÙóÖ® Ž#Ò4íÊZö2Íý¢ )`¶-˜ K�;Íu×]Æ#�Üɲeu̪=iÔ¬õîDýæ«Êõ.tìä\¶vï½ÿÞ=æâ‹WŽÉÉdjr¨Û·v–y¾·4uLN¦$IÊ?¸Šýû_â®»öN}HBÑ�z·³ �¾U¹Þ…�ü$ÌË/âê«ï¥··Þ•Ý@KA­qâÄ�>z×\óɦɰêR½«Qï–Úÿ¡‡0::qîÝ2fù,j9o­ß{6LܳçOù[,ï�°]íÖ;"—÷Öú½W§Þ…íô3±¼Foo�4m½6H'“¢^rQD-’>M}}=<ø6ccïÓ×W� ¢¹´[ïFRîQA3«X¥zþÍ�søð)êõ¸å¶‘™Ñ7Ð_Ê_”71:Ž›ñawÎQ¯Ç¼ûîŽ9ņ CSk‰*j¿Þ0xQOVï’æÀðxƒÄ¹³B Jõ.ô@ $I[® ÌŒd2¡o Ÿ�÷ÿ;}ý$“I)ƒàÁ]?çø[G¨÷öÌÚæMSwÎ5a�»ÞÙš°¿—ß캆Áþ]8¬nù铼zø}=5Ò Ö»Ð8_�ý åE¥ +øh³2;½³À.ꥑ”3b³²^ÚÒÕ/A6�ä·2€ÌO#qS·2–»ÊÍ‹3÷Hµµ3«.‹§œ;/E¤ ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` €‚r^L ¬÷ZU €‚•õì°óá¿Ö›]ÿoq—E.Lõ?­]äœcddîKYUAšfŽŒŒ“”ü¤Ÿ¡ëþ A*nǎݬ^}Ñb/FW �3<}_Í_r €‚E‘M5~u ;걪ï/$š ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)D¦ ˜@$` ‘€)DVè AÌ kq¹XçQ11:΃»~Ž•øD§Ž¼G\o}þ»ìý/ÂB-’s×âÈopËOŸÌNRÒ3ýýÝzk1iEë]HøÁŠËZΑ#'éé™}ýx—:Ž¿u4?¥L¯Ü}q½6ëCof4“ °ví@~ßb,]w´Soçxõð©²ö>½µxV-«TïÂFI’Ç[¶là™gÞdùò^Òtvéë½õ¢^rQ´ZóG‘1>ÞàòË׳|ù²JŸÐk§Þôõ”û¬s­ÖüUªwásÛ·o¥^�Ïy²ÈìÚyå½µü%FF£‘°cÇç¦Þc(ÎWïÔ¹RßZ©R½ €8ŽHSÇ–-عóó=zŠž’§;zzj?~šnø7ÞøiÒÔÇÕŸ[U½«QosÆ—¿2îøxƒmÛîgÿþ—X·nçȇ‡Õ¸r®?#nÇ�³iÓz{ì;¬Y³p-'ƪHõ.½ ˜>güÈÈ»víeß¾§0ƒeËz¨å*Ùi¥‹|Õîq’$a|¼A£‘pýõW°{÷NÖ­[YêmÁ…R½KúÆr…L(üEöî=ÀsϽ͉gHS7õ¡(#3cÕª~6mZ϶mŸáæ›·TâðPªwyu$`zxèI££=zš$IK=dZÁÀ@à?ØåAõ.'sÎ%tðˆÀ$IJ=QÒJšf{ªö¾.”ê]*©9çŽCdÇju,Öü¢ ̨Ì STï%Í÷úñp†,:ª ‡MJûTïR8oçÿ¨H^‹Èyø^;þ:ãN©6ßë�€sIDdѼ��@$YÏŒ€ç�£d³‚Ú ©6¿à(ð|dfÃÀ3ù¦‹¶X"Ò ¾ÇŸ1³áÈ9gÀoó;5©6ßã¿uÎe{j�s^aú!Úƒ+R=¾ù'�O˜Ù‘s.2³7€?æÿ©Í‘jò½ýG3{Ã9ELÏþÿ­ùEªÎÈz ²|`ð*°žl¨ Ý‚"Õ‘’5ÿ;ÀÇ�q€ÈÌ›Ùðc´;P¤Šüî¿ç½›™ó“€~ÐO6 øˆT…_û"[û�˜™‹ü_Èaø~þ`MŠTƒ€ïç=ç=?=é—� ˆ�g�+€$ÿ·ˆ”“ïáç�-ù¿�€©!~~‡™Y¸ƒlÀßD¤|š{øŽ¼·Í7?ÌØÆ7³Ä9W3³ÀÉ Jº¸À"Rœ„¬‡hfòÞ>«Ÿgí÷Ï7üñÿ|m ˆ”�ïÙ§�&›H›×þpŽò£SçÜGȾ.¼í) ?é÷°ÙÌÞò==ó�-:oþØÌÞnaz@{D–6ߣ¸%oþ¸UóÃkô¦ù€'�¯5=V! ²4ùÞŒ€¯™Ù“­¶û›�÷Øÿü &�sÛ�‡˜žUÔæ€ÈÒá‡ýì0³}¾wçú¡ó6qÞüu3Ûì ›\ˆÐÞ‘¥¢¹'}ó×Ï×ü0�oÿ5�®ökȾW\ýkB‹,]¾ÿl7³'ÚYó{mã󿝙ÙÀVàÏù ;4/ Òm)YïÕÈzqë|›æ¹Ÿ‡@lf¯_~Ðô<“(D:-%ë5ß»?¾hf¯ç½ÙvóÃOÒ¼OÑ9÷àGdÇÓ´pš$)Nšßü&÷³ÀwÍìpvOÎǂϔ1ûMàÛÀ.`Cþ?I]È눬yóÚ‰û&ð`wSï%3�ðk×7f>ìHò¿ßî>Öô°Éüµ"sóMï·ï½×�û�_šÙi8»÷ª�fôßh ‚~àfàëÀ5@}Æ�ø…öû- [‘’pMú¿Ïü¾Mø=°ø•™�BÖø´8®! mº™A�ß·¸ø2°™ìœƒ"ÒÚ;dß¿ùð„™ù‹÷Úø^GÖº~~€laÓ¦ûW—‘Á&àRà`90„&% )p8ü x�ì"½�—Ì줠sÎO¨/x;.ÿ6]Üœe¼vÜIEND®B`‚trizbort-qt-1.0.1/packaging/trizbort-qt.rc.in000066400000000000000000000006631526521612000211770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2026 Jason Self // SPDX-License-Identifier: GPL-3.0-or-later // // Windows resource script: gives the .exe its file icon in Explorer and the // title-bar/taskbar icon. CMake fills in @TRIZBORT_ICON_PATH@ with the absolute // path to packaging/trizbort-qt.ico at configure time, so the resource compiler // resolves it regardless of its working directory. IDI_ICON1 ICON "@TRIZBORT_ICON_PATH@" trizbort-qt-1.0.1/packaging/trizbort-qt.svg000066400000000000000000000031161526521612000207610ustar00rootroot00000000000000 trizbort-qt-1.0.1/packaging/trizbort-qt.xml000066400000000000000000000021731526521612000207640ustar00rootroot00000000000000 Trizbort interactive-fiction map trizbort-qt-1.0.1/src/000077500000000000000000000000001526521612000146015ustar00rootroot00000000000000trizbort-qt-1.0.1/src/AppSettings.h000066400000000000000000000074701526521612000172230ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_APPSETTINGS_H #define TRIZBORT_APPSETTINGS_H #include namespace trizbort { // Application-wide preferences (the C# ApplicationSettingsController.AppSettings), // shared as a single instance so rendering code — tooltips, hand-drawn defaults, // export margins, port granularity — can read them without threading a pointer // through every object. MainWindow loads and saves these via QSettings; the // defaults here match the C# ones so headless tools behave like a fresh install. struct AppSettings { // Tooltips. bool showObjectsInTooltips = true; bool showDescriptionsInTooltips = true; bool limitRoomDescriptionChars = false; int roomDescriptionChars = 50; bool limitConnectionDescriptionChars = false; int connectionDescriptionChars = 50; // Rendering. bool handDrawnGlobal = false; // seeds new rooms' hand-drawn flag int portAdjustDetail = 16; // compass points offered when docking: 16/8/4 QString defaultFontName = QStringLiteral("Arial"); // Canvas interaction. bool infiniteScrollBounds = false; // scroll far beyond the content bounds // General export margins, used when a document has no document-specific // margins of its own. bool specifyGenMargins = false; double genHorizontalMargin = 0.0; double genVerticalMargin = 0.0; bool showTooltips() const { return showObjectsInTooltips || showDescriptionsInTooltips; } static AppSettings &instance() { static AppSettings s; return s; } }; } // namespace trizbort #endif // TRIZBORT_APPSETTINGS_H trizbort-qt-1.0.1/src/AutomapGui.h000066400000000000000000000325611526521612000170340ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_AUTOMAPGUI_H #define TRIZBORT_AUTOMAPGUI_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TranscriptAutomapper.h" namespace trizbort { // The GUI implementation of the automapper's decision callbacks: modal // disambiguation and same-direction dialogs, and an F11 single-step gate run on // a nested event loop so the window stays responsive while mapping. class GuiAutomapController : public AutomapController { public: explicit GuiAutomapController(QWidget *parent) : m_parent(parent) { } void setStepping(bool stepping) { m_stepping = stepping; } bool stepping() const { return m_stepping; } // id -> short label shown in the disambiguation list. void setLabelFn(std::function fn) { m_labelFn = std::move(fn); } void setStatusFn(std::function fn) { m_statusFn = std::move(fn); } // Called by the window while an automap is running. void proceed() { if (m_loop) m_loop->quit(); } void runToCompletion() { m_stepping = false; if (m_loop) m_loop->quit(); } void cancel() { m_cancel = true; if (m_loop) m_loop->quit(); } bool canceled() const { return m_cancel; } int disambiguateRoom(const RoomQuery &query, const QList &candidates, bool *stopAsking) override { QDialog dialog(m_parent); dialog.setWindowTitle(QObject::tr("Disambiguate Rooms")); auto *layout = new QVBoxLayout(&dialog); auto *intro = new QLabel( QObject::tr("Trizbort isn't sure which room the transcript is referring to.\n\n" "Either several rooms have the same name but different descriptions, " "or the room description has changed during play."), &dialog); intro->setWordWrap(true); layout->addWidget(intro); layout->addWidget(new QLabel(QObject::tr("When the transcript says:"), &dialog)); auto *context = new QPlainTextEdit(&dialog); context->setReadOnly(true); context->setPlainText(query.line + QLatin1Char('\n') + query.description); layout->addWidget(context); layout->addWidget(new QLabel(QObject::tr("Which room does it mean?"), &dialog)); auto *row = new QHBoxLayout; auto *list = new QListWidget(&dialog); for (const RoomCandidate &c : candidates) { const QString label = m_labelFn ? m_labelFn(c.id) : QObject::tr("Room %1").arg(c.id); auto *item = new QListWidgetItem(label, list); item->setData(Qt::UserRole, c.id); item->setData(Qt::UserRole + 1, c.description); } auto *description = new QPlainTextEdit(&dialog); description->setReadOnly(true); row->addWidget(list, 1); row->addWidget(description, 2); layout->addLayout(row); auto *buttons = new QDialogButtonBox(&dialog); QPushButton *thisRoom = buttons->addButton(QObject::tr("&This Room"), QDialogButtonBox::AcceptRole); QPushButton *newRoom = buttons->addButton(QObject::tr("A &New Room"), QDialogButtonBox::RejectRole); QPushButton *autoChoose = buttons->addButton(QObject::tr("&Auto-Choose the Rest"), QDialogButtonBox::DestructiveRole); thisRoom->setEnabled(false); layout->addWidget(buttons); int result = -1; QObject::connect(list, &QListWidget::currentItemChanged, &dialog, [&](QListWidgetItem *current, QListWidgetItem *) { thisRoom->setEnabled(current != nullptr); description->setPlainText( current ? current->data(Qt::UserRole + 1).toString() : QString()); }); QObject::connect(thisRoom, &QPushButton::clicked, &dialog, [&] { if (QListWidgetItem *item = list->currentItem()) { result = item->data(Qt::UserRole).toInt(); dialog.accept(); } }); QObject::connect(list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) { result = item->data(Qt::UserRole).toInt(); dialog.accept(); }); QObject::connect(newRoom, &QPushButton::clicked, &dialog, [&] { result = -1; dialog.reject(); }); QObject::connect(autoChoose, &QPushButton::clicked, &dialog, [&] { if (stopAsking) *stopAsking = true; result = candidates.isEmpty() ? -1 : candidates.first().id; dialog.accept(); }); if (candidates.size() == 1) list->setCurrentRow(0); dialog.exec(); return result; } SameDir sameDirection(int fromRoomId, int existingRoomId, const QString &dir, const QString &newRoomName) override { Q_UNUSED(fromRoomId); Q_UNUSED(dir); const QString existingName = m_labelFn ? m_labelFn(existingRoomId) : QObject::tr("Room %1").arg(existingRoomId); QMessageBox box(m_parent); box.setWindowTitle(QObject::tr("Room Already There")); box.setText(QObject::tr("Room '%1' is already defined in the same direction as room '%2'. " "What would you like to do?") .arg(existingName, newRoomName)); QPushButton *existing = box.addButton(QObject::tr("Keep '%1'").arg(existingName), QMessageBox::AcceptRole); QPushButton *fresh = box.addButton(QObject::tr("Keep '%1'").arg(newRoomName), QMessageBox::AcceptRole); QPushButton *both = box.addButton(QObject::tr("Keep Both"), QMessageBox::AcceptRole); box.setDefaultButton(both); box.setEscapeButton(existing); box.exec(); if (box.clickedButton() == existing) return SameDir::KeepExisting; if (box.clickedButton() == fresh) return SameDir::KeepNew; return SameDir::KeepBoth; } bool step() override { if (m_cancel) return false; if (!m_stepping) return true; QEventLoop loop; m_loop = &loop; if (m_statusFn) m_statusFn(QObject::tr("Automap paused — F11 steps, Shift+F11 runs to the end, Stop cancels.")); loop.exec(); m_loop = nullptr; return !m_cancel; } void status(const QString &message) override { if (m_statusFn) m_statusFn(message); } private: QWidget *m_parent = nullptr; bool m_stepping = false; bool m_cancel = false; QEventLoop *m_loop = nullptr; std::function m_labelFn; std::function m_statusFn; }; // The options dialog shown before automapping (the C# AutomapDialog): transcript // file, the toggles, and the customizable command words. class AutomapOptionsDialog : public QDialog { public: explicit AutomapOptionsDialog(const AutomapSettings &settings, QWidget *parent = nullptr) : QDialog(parent) , m_settings(settings) { setWindowTitle(tr("Automap")); auto *form = new QFormLayout(this); auto *fileRow = new QWidget(this); auto *fileLayout = new QHBoxLayout(fileRow); fileLayout->setContentsMargins(0, 0, 0, 0); m_file = new QLineEdit(fileRow); auto *browse = new QPushButton(tr("Browse…"), fileRow); fileLayout->addWidget(m_file); fileLayout->addWidget(browse); connect(browse, &QPushButton::clicked, this, [this] { const QString p = QFileDialog::getOpenFileName( this, tr("Transcript"), QString(), tr("Transcripts (*.txt *.log *.scr);;All files (*)")); if (!p.isEmpty()) m_file->setText(p); }); form->addRow(tr("&Transcript:"), fileRow); m_twoWay = new QCheckBox(tr("Assume connections are two-way"), this); m_twoWay->setChecked(settings.assumeTwoWayConnections); form->addRow(QString(), m_twoWay); m_sameName = new QCheckBox(tr("Rooms with the same name are the same room"), this); m_sameName->setChecked(settings.assumeSameNameSameRoom); form->addRow(QString(), m_sameName); m_verbose = new QCheckBox(tr("Transcript is verbose; expect room descriptions"), this); m_verbose->setChecked(settings.verboseTranscript); form->addRow(QString(), m_verbose); m_guessExits = new QCheckBox(tr("Guess exits from room descriptions"), this); m_guessExits->setChecked(settings.guessExits); form->addRow(QString(), m_guessExits); m_startFromEnd = new QCheckBox(tr("Start from the end of the transcript (map only new moves)"), this); m_startFromEnd->setChecked(settings.continueTranscript); form->addRow(QString(), m_startFromEnd); m_step = new QCheckBox(tr("Single-step with F11"), this); form->addRow(QString(), m_step); m_objectCmd = new QLineEdit(settings.objectCommand, this); form->addRow(tr("&Add-object command:"), m_objectCmd); m_regionCmd = new QLineEdit(settings.regionCommand, this); form->addRow(tr("Add-®ion command:"), m_regionCmd); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); form->addRow(buttons); } QString transcriptPath() const { return m_file->text().trimmed(); } bool stepping() const { return m_step->isChecked(); } AutomapSettings settings() const { AutomapSettings s = m_settings; s.assumeTwoWayConnections = m_twoWay->isChecked(); s.assumeSameNameSameRoom = m_sameName->isChecked(); s.verboseTranscript = m_verbose->isChecked(); s.continueTranscript = m_startFromEnd->isChecked(); s.guessExits = m_guessExits->isChecked(); s.objectCommand = m_objectCmd->text().trimmed().isEmpty() ? QStringLiteral("tb see") : m_objectCmd->text().trimmed(); s.regionCommand = m_regionCmd->text().trimmed().isEmpty() ? QStringLiteral("tb region") : m_regionCmd->text().trimmed(); return s; } private: AutomapSettings m_settings; QLineEdit *m_file = nullptr; QCheckBox *m_twoWay = nullptr; QCheckBox *m_sameName = nullptr; QCheckBox *m_verbose = nullptr; QCheckBox *m_startFromEnd = nullptr; QCheckBox *m_guessExits = nullptr; QCheckBox *m_step = nullptr; QLineEdit *m_objectCmd = nullptr; QLineEdit *m_regionCmd = nullptr; }; } // namespace trizbort #endif // TRIZBORT_AUTOMAPGUI_H trizbort-qt-1.0.1/src/ColorButton.h000066400000000000000000000063731526521612000172350ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #ifndef TRIZBORT_COLORBUTTON_H #define TRIZBORT_COLORBUTTON_H #include #include #include #include #include #include #include namespace trizbort { // A push button that edits an optional color. Left-click opens a color // picker; when clearing is allowed, right-click offers "clear" to fall back to // the default (an invalid QColor). Header-only and free of new signals, so it // needs no moc. class ColorButton : public QPushButton { public: explicit ColorButton(QWidget *parent = nullptr) : QPushButton(parent) { connect(this, &QPushButton::clicked, this, [this] { choose(); }); refresh(); } void setColor(const QColor &c) { m_color = c; refresh(); } QColor color() const { return m_color; } void setAllowClear(bool allow) { m_allowClear = allow; refresh(); } void setTitle(const QString &title) { m_title = title; } protected: void contextMenuEvent(QContextMenuEvent *event) override { if (!m_allowClear) { QPushButton::contextMenuEvent(event); return; } QMenu menu(this); QAction *setAct = menu.addAction(tr("Set Color…")); QAction *clearAct = menu.addAction(tr("Clear (use default)")); clearAct->setEnabled(m_color.isValid()); QAction *chosen = menu.exec(event->globalPos()); if (chosen == setAct) choose(); else if (chosen == clearAct) setColor(QColor()); } private: void choose() { const QColor start = m_color.isValid() ? m_color : QColor(Qt::white); const QColor c = QColorDialog::getColor( start, this, m_title.isEmpty() ? tr("Choose Color") : m_title); if (c.isValid()) setColor(c); } void refresh() { if (m_color.isValid()) { QPixmap pm(16, 16); pm.fill(m_color); setIcon(QIcon(pm)); setText(m_color.name().toUpper()); } else { setIcon(QIcon()); setText(m_allowClear ? tr("None (default)") : tr("Default (click to set)")); } if (m_allowClear) setToolTip(tr("Left-click to set a color; right-click to clear.")); } QColor m_color; QString m_title; bool m_allowClear = true; }; } // namespace trizbort #endif // TRIZBORT_COLORBUTTON_H trizbort-qt-1.0.1/src/ConnectionDialog.cpp000066400000000000000000000154241526521612000205320ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "ConnectionDialog.h" #include #include #include #include #include #include #include #include "ColorButton.h" namespace trizbort { ConnectionDialog::ConnectionDialog(const Connection &connection, QWidget *parent) : QDialog(parent) , m_conn(connection) { setWindowTitle(tr("Connection Properties")); auto *form = new QFormLayout(this); m_flow = new QComboBox(this); m_flow->addItems({tr("Two-way"), tr("One-way")}); m_flow->setCurrentIndex(connection.flow == ConnectionFlow::OneWay ? 1 : 0); form->addRow(tr("&Flow:"), m_flow); m_style = new QComboBox(this); m_style->addItems({tr("Solid"), tr("Dashed")}); m_style->setCurrentIndex(connection.style == ConnectionStyle::Dashed ? 1 : 0); form->addRow(tr("&Style:"), m_style); m_colorButton = new ColorButton(this); m_colorButton->setTitle(tr("Connection Color")); m_colorButton->setColor(connection.color); form->addRow(tr("&Color:"), m_colorButton); m_name = new QLineEdit(connection.name, this); form->addRow(tr("&Name:"), m_name); m_description = new QLineEdit(connection.description, this); form->addRow(tr("&Description:"), m_description); m_startText = new QLineEdit(connection.startText, this); form->addRow(tr("Start &text:"), m_startText); m_midText = new QLineEdit(connection.midText, this); form->addRow(tr("&Mid text:"), m_midText); m_endText = new QLineEdit(connection.endText, this); form->addRow(tr("&End text:"), m_endText); // Quick up/down/in/out labels: set the start/end text to a direction pair // (the C# connection labels), which is what those connections render as. auto *quick = new QWidget(this); auto *quickLayout = new QHBoxLayout(quick); quickLayout->setContentsMargins(0, 0, 0, 0); const struct { const char *label; const char *start; const char *end; } kQuick[] = {{"Up/Down", "up", "down"}, {"Down/Up", "down", "up"}, {"In/Out", "in", "out"}, {"Out/In", "out", "in"}}; for (const auto &q : kQuick) { auto *btn = new QPushButton(tr(q.label), quick); const QString s = QString::fromLatin1(q.start); const QString e = QString::fromLatin1(q.end); connect(btn, &QPushButton::clicked, this, [this, s, e] { m_startText->setText(s); m_endText->setText(e); }); quickLayout->addWidget(btn); } form->addRow(tr("Quick &label:"), quick); m_door = new QCheckBox(tr("Has door"), this); m_door->setChecked(connection.hasDoor); form->addRow(QString(), m_door); m_lockable = new QCheckBox(tr("Lockable"), this); m_lockable->setChecked(connection.door.lockable); form->addRow(QString(), m_lockable); m_openable = new QCheckBox(tr("Openable"), this); m_openable->setChecked(connection.door.openable); form->addRow(QString(), m_openable); m_locked = new QCheckBox(tr("Locked"), this); m_locked->setChecked(connection.door.locked); form->addRow(QString(), m_locked); m_open = new QCheckBox(tr("Open"), this); m_open->setChecked(connection.door.open); form->addRow(QString(), m_open); auto updateDoorEnabled = [this] { const bool on = m_door->isChecked(); m_lockable->setEnabled(on); m_openable->setEnabled(on); m_locked->setEnabled(on); m_open->setEnabled(on); }; connect(m_door, &QCheckBox::toggled, this, updateDoorEnabled); updateDoorEnabled(); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); form->addRow(buttons); } Connection ConnectionDialog::result() const { Connection c = m_conn; c.flow = m_flow->currentIndex() == 1 ? ConnectionFlow::OneWay : ConnectionFlow::TwoWay; c.style = m_style->currentIndex() == 1 ? ConnectionStyle::Dashed : ConnectionStyle::Solid; c.name = m_name->text(); c.description = m_description->text(); c.color = m_colorButton->color(); c.startText = m_startText->text(); c.midText = m_midText->text(); c.endText = m_endText->text(); c.hasDoor = m_door->isChecked(); c.door.lockable = m_lockable->isChecked(); c.door.openable = m_openable->isChecked(); c.door.locked = m_locked->isChecked(); c.door.open = m_open->isChecked(); return c; } } // namespace trizbort trizbort-qt-1.0.1/src/ConnectionDialog.h000066400000000000000000000064401526521612000201750ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_CONNECTIONDIALOG_H #define TRIZBORT_CONNECTIONDIALOG_H #include #include "MapDocument.h" class QLineEdit; class QComboBox; class QCheckBox; namespace trizbort { class ColorButton; // Modal editor for a connection's properties. result() returns the edited // connection (id, seq and vertices preserved). class ConnectionDialog : public QDialog { Q_OBJECT public: ConnectionDialog(const Connection &connection, QWidget *parent = nullptr); Connection result() const; private: Connection m_conn; QComboBox *m_flow = nullptr; QComboBox *m_style = nullptr; QLineEdit *m_name = nullptr; QLineEdit *m_description = nullptr; QLineEdit *m_startText = nullptr; QLineEdit *m_midText = nullptr; QLineEdit *m_endText = nullptr; ColorButton *m_colorButton = nullptr; QCheckBox *m_door = nullptr; QCheckBox *m_lockable = nullptr; QCheckBox *m_openable = nullptr; QCheckBox *m_locked = nullptr; QCheckBox *m_open = nullptr; }; } // namespace trizbort #endif // TRIZBORT_CONNECTIONDIALOG_H trizbort-qt-1.0.1/src/ConnectionItem.cpp000066400000000000000000000655171526521612000202410ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "ConnectionItem.h" #include #include #include #include #include #include #include #include #include #include #include #include "AppSettings.h" #include "EditCommands.h" #include "FontUtil.h" #include "LabelGeometry.h" #include "MapScene.h" namespace trizbort { namespace { // The point half way along a poly-line by arc length. QPointF midpointAlongPath(const QList &pts) { if (pts.size() < 2) return pts.isEmpty() ? QPointF() : pts.first(); double total = 0.0; for (int i = 1; i < pts.size(); ++i) total += std::hypot(pts[i].x() - pts[i - 1].x(), pts[i].y() - pts[i - 1].y()); double half = total / 2.0; for (int i = 1; i < pts.size(); ++i) { const QPointF d = pts[i] - pts[i - 1]; const double len = std::hypot(d.x(), d.y()); if (half <= len && len > 0.0) return pts[i - 1] + d * (half / len); half -= len; } return pts.last(); } // The midpoint of a poly-line together with the unit direction of the segment // it falls on, so mid-line labels can be oriented like Connection.annotate. struct MidInfo { QPointF point; QPointF unit{0, 1}; // segment direction (start -> end) }; MidInfo midSegmentInfo(const QList &pts) { MidInfo info; if (pts.size() < 2) { info.point = pts.isEmpty() ? QPointF() : pts.first(); return info; } double total = 0.0; for (int i = 1; i < pts.size(); ++i) total += std::hypot(pts[i].x() - pts[i - 1].x(), pts[i].y() - pts[i - 1].y()); double half = total / 2.0; for (int i = 1; i < pts.size(); ++i) { const QPointF d = pts[i] - pts[i - 1]; const double len = std::hypot(d.x(), d.y()); if (len > 0.0 && half <= len) { info.point = pts[i - 1] + d * (half / len); info.unit = d / len; return info; } half -= len; } info.point = pts.last(); return info; } // Ellipse/octagonal rooms pull their end labels a few pixels off the outline, // per-port, matching Connection.roomTypeAdjustments. Zero for other shapes. QPointF roomTypeLabelAdjust(const Map *map, const Connection &c, bool start) { if (!map || c.vertices.isEmpty()) return QPointF(); QList sorted = c.vertices; std::sort(sorted.begin(), sorted.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); const Vertex &v = start ? sorted.first() : sorted.last(); if (!v.docked) return QPointF(); const Room *r = map->roomById(v.roomId); if (!r || !(r->ellipse || r->octagonal)) return QPointF(); const QString p = v.port.toLower(); if (p == QLatin1String("se")) return QPointF(8, 6); if (p == QLatin1String("ne")) return QPointF(10, -6); if (p == QLatin1String("ene") || p == QLatin1String("ese")) return QPointF(4, 0); if (p == QLatin1String("nw") || p == QLatin1String("wnw")) return QPointF(-10, -4); if (p == QLatin1String("sw")) return QPointF(-10, 4); if (p == QLatin1String("wsw")) return QPointF(-10, 0); return QPointF(); } enum class LabelAlign { Near, Center, Far }; // inRange() and labelCompassFromDelta() now live in LabelGeometry.h so the // --geometry-selftest can exercise the exact angle table the renderer uses. void alignmentForCompass(const QString &compass, LabelAlign &horizontal, LabelAlign &vertical) { if (compass == QLatin1String("n") || compass == QLatin1String("ne")) { vertical = LabelAlign::Far; horizontal = LabelAlign::Near; } else if (compass == QLatin1String("e") || compass == QLatin1String("se") || compass == QLatin1String("s")) { vertical = LabelAlign::Near; horizontal = LabelAlign::Near; } else if (compass == QLatin1String("w") || compass == QLatin1String("sw")) { vertical = LabelAlign::Near; horizontal = LabelAlign::Far; } else if (compass == QLatin1String("nw") || compass == QLatin1String("nnw")) { vertical = LabelAlign::Far; horizontal = LabelAlign::Far; } else { vertical = LabelAlign::Near; horizontal = LabelAlign::Near; } } void drawAlignedText(QPainter *painter, const QPointF &pos, const QString &text, const QFont &font, const QColor &color, LabelAlign horizontal, LabelAlign vertical) { if (text.isEmpty()) return; painter->setFont(font); painter->setPen(color); const QFontMetricsF fm(font); const QRectF box = fm.boundingRect(QRectF(0, 0, 1000, 1000), Qt::AlignLeft | Qt::TextWordWrap, text); const double w = box.width(); const double h = box.height(); double left = pos.x(); if (horizontal == LabelAlign::Center) left = pos.x() - w / 2.0; else if (horizontal == LabelAlign::Far) left = pos.x() - w; double top = pos.y(); if (vertical == LabelAlign::Center) top = pos.y() - h / 2.0; else if (vertical == LabelAlign::Far) top = pos.y() - h; painter->drawText(QRectF(left, top, w, h), Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, text); } void drawConnectionLabel(QPainter *painter, const QPointF &anchor, const QPointF &delta, LabelAlign placement, const QString &text, const QFont &font, const QColor &color, double offset) { if (text.isEmpty()) return; double angle = 0.0; const QString compass = labelCompassFromDelta(delta, angle); const double x = anchor.x() - offset; const double y = anchor.y() - offset; const double size = 2.0 * offset; QPointF pos = MapScene::squareCorner(x, y, size, size, compass); LabelAlign horizontal = LabelAlign::Near; LabelAlign vertical = LabelAlign::Near; alignmentForCompass(compass, horizontal, vertical); if (placement == LabelAlign::Center && std::abs(std::abs(angle) - 90.0) < 1e-6) { pos = MapScene::squareCorner(x, y, size, size, QStringLiteral("e")); vertical = LabelAlign::Center; } if (placement == LabelAlign::Center && inRange(angle, -10, 10)) { pos = MapScene::squareCorner(x, y, size, size, QStringLiteral("s")); horizontal = LabelAlign::Center; vertical = LabelAlign::Near; } drawAlignedText(painter, pos, text, font, color, horizontal, vertical); } // A small upright door glyph centerd on pos: a solid leaf when closed, an open // (outline) leaf when open. void drawDoorGlyph(QPainter *painter, const QPointF &pos, const QColor &color, bool open) { const QRectF leaf(pos.x() - 5, pos.y() - 7, 10, 14); painter->setPen(QPen(color, 1.5)); painter->setBrush(open ? QBrush(Qt::white) : QBrush(color)); painter->drawRect(leaf); // Door knob. painter->setPen(QPen(open ? color : QColor(Qt::white), 1.0)); painter->setBrush(Qt::NoBrush); painter->drawPoint(QPointF(leaf.right() - 2.5, pos.y())); } // A small padlock glyph centerd on pos: closed shackle when locked, an open // (offset) shackle when unlocked. void drawLockGlyph(QPainter *painter, const QPointF &pos, const QColor &color, bool locked) { const QRectF body(pos.x() - 4, pos.y() - 1, 8, 7); painter->setPen(QPen(color, 1.2)); painter->setBrush(QBrush(color)); painter->drawRect(body); painter->setBrush(Qt::NoBrush); if (locked) { // Closed shackle sitting on top of the body. painter->drawArc(QRectF(pos.x() - 3, pos.y() - 6, 6, 8), 0 * 16, 180 * 16); } else { // Open shackle, pivoted up to one side. painter->drawArc(QRectF(pos.x() - 1, pos.y() - 7, 6, 8), 30 * 16, 160 * 16); } } } // namespace ConnectionItem::ConnectionItem(MapScene *scene, int connId) : m_scene(scene) , m_connId(connId) { setFlags(ItemIsSelectable); setAcceptHoverEvents(true); setZValue(-1); updateRoute(); } void ConnectionItem::updateRoute() { prepareGeometryChange(); m_points.clear(); const Map *map = m_scene->document(); if (!map) return; const int idx = map->connectionIndex(m_connId); if (idx < 0) return; const Connection &c = map->connections.at(idx); QList vertices = c.vertices; std::sort(vertices.begin(), vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); // Build the poly-line through every vertex, and where an endpoint docks to a // room, run an outward "stalk" between the room outline and the endpoint so // the line leaves the room perpendicular to its edge (matching the C# // Connection.getSegments stalk handling). const double stalk = map->settings.connectionStalkLength; const qsizetype n = vertices.size(); for (qsizetype i = 0; i < n; ++i) { const Vertex &v = vertices.at(i); const Room *room = v.docked ? map->roomById(v.roomId) : nullptr; const QPointF base = (room != nullptr) ? MapScene::portPoint(*room, v.port) : v.point; QPointF sp; bool haveStalk = false; if (room != nullptr && n >= 2 && stalk > 0.0) { sp = MapScene::portStalkPoint(*room, v.port, stalk); haveStalk = (sp != base); } if (i == n - 1 && haveStalk) // last endpoint: the stalk precedes it m_points.append(sp); m_points.append(base); if (i == 0 && haveStalk) // first endpoint: the stalk follows it m_points.append(sp); } // Hover tooltip: name, a door marker, and — per the application settings — // the description (optionally truncated). const AppSettings &app = AppSettings::instance(); QString tip = c.name.isEmpty() ? QObject::tr("Connection") : c.name; if (c.hasDoor) tip += QObject::tr(" (Door)"); if (app.showDescriptionsInTooltips && !c.description.trimmed().isEmpty()) { QString desc = c.description.trimmed(); if (app.limitConnectionDescriptionChars && app.connectionDescriptionChars > 0 && desc.length() > app.connectionDescriptionChars) desc = desc.left(app.connectionDescriptionChars).trimmed() + QStringLiteral("…"); tip += QLatin1Char('\n') + desc; } setToolTip(tip); update(); } QRectF ConnectionItem::boundingRect() const { if (m_points.isEmpty()) return QRectF(); QRectF r(m_points.first(), m_points.first()); for (const QPointF &p : m_points) r = r.united(QRectF(p, p)); // Margin covers the line width, arrowheads, door/lock glyphs and short // end/mid labels drawn just off the line. return r.adjusted(-40, -40, 40, 40); } QPainterPath ConnectionItem::shape() const { QPainterPath path; if (m_points.size() < 2) return path; path.moveTo(m_points.first()); for (int i = 1; i < m_points.size(); ++i) path.lineTo(m_points.at(i)); QPainterPathStroker stroker; stroker.setWidth(20); return stroker.createStroke(path); } void ConnectionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (m_points.size() < 2) return; const Map *map = m_scene->document(); const int idx = map ? map->connectionIndex(m_connId) : -1; if (idx < 0) return; const Connection &c = map->connections.at(idx); QColor color = c.color.isValid() ? c.color : map->lineColor(); if (!color.isValid()) color = QColor(Qt::darkBlue); QPen pen(color); pen.setWidthF(2.0); // C# renders a "dashed" connection with a dotted pen (Palette.DashedLinePen // uses DashStyle.Dot), so match that rather than drawing long dashes. if (c.style == ConnectionStyle::Dashed) pen.setStyle(Qt::DotLine); if (isSelected()) { QColor sel = map->settings.colors[ColorSelectedLine]; pen.setColor(sel.isValid() ? sel : QColor(30, 120, 220)); pen.setWidthF(3.0); } else if (m_hover) { // Hover highlight in the palette's hover-line color (C# HoverLine pen). QColor hov = map->settings.colors[ColorHoverLine]; pen.setColor(hov.isValid() ? hov : QColor(0, 150, 0)); pen.setWidthF(3.0); } painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(pen); painter->setBrush(Qt::NoBrush); // "Smart" line segments: draw a small gap wherever this line crosses another // connection, so crossings read clearly. To avoid gapping both lines of a // pair, only the higher-id connection gaps (against lower-id ones). const QVector> others = m_scene->connectionSegmentsNear(this); const double gap = 4.0; for (int i = 1; i < m_points.size(); ++i) { const QPointF a = m_points.at(i - 1); const QPointF b = m_points.at(i); const QLineF seg(a, b); const double len = seg.length(); QVector ts; if (len > 1e-6) { for (const QPair &o : others) { if (o.second >= m_connId) continue; // the other (lower id) will gap instead QPointF ip; if (seg.intersects(o.first, &ip) == QLineF::BoundedIntersection) { const double t = QLineF(a, ip).length() / len; if (t > 0.03 && t < 0.97) // ignore near shared endpoints ts.append(t); } } } if (ts.isEmpty()) { painter->drawLine(a, b); continue; } std::sort(ts.begin(), ts.end()); const double gt = gap / len; double cursor = 0.0; for (double t : ts) { const double stop = std::max(cursor, t - gt); if (stop > cursor) painter->drawLine(a + (b - a) * cursor, a + (b - a) * stop); cursor = std::min(1.0, t + gt); } if (cursor < 1.0) painter->drawLine(a + (b - a) * cursor, b); } // One-way chevrons: a ">"-shaped arrowhead at the midpoint of every segment // long enough to hold one, pointing along the flow direction. This mirrors // the C# Connection.Draw, which draws a chevron per segment sized from // Settings.ConnectionArrowSize, rather than a single arrow at the end. if (c.flow == ConnectionFlow::OneWay) { const double arrow = map->settings.arrowSize > 0.0 ? map->settings.arrowSize : 12.0; QPen arrowPen(color); arrowPen.setWidthF(pen.widthF()); painter->setPen(arrowPen); painter->setBrush(Qt::NoBrush); for (int i = 1; i < m_points.size(); ++i) { const QPointF a = m_points.at(i - 1); const QPointF b = m_points.at(i); const QPointF d = b - a; const double len = std::hypot(d.x(), d.y()); if (len <= arrow) continue; const QPointF u = d / len; const QPointF nrm(-u.y(), u.x()); const QPointF mid = (a + b) / 2.0; const double s = arrow / 2.0; const QPointF apex = mid + u * s; const QPointF right = mid - u * s + nrm * s; const QPointF left = mid - u * s - nrm * s; QPolygonF chevron; chevron << apex << right << left; painter->drawPolygon(chevron); } } // A door is shown as a door-leaf glyph plus a padlock reflecting the // open/locked state, at the middle of the connection (matching the C# // door + lock icon pair). if (c.hasDoor) { const QPointF mid = midpointAlongPath(m_points); drawDoorGlyph(painter, mid - QPointF(7, 0), color, c.door.open); drawLockGlyph(painter, mid + QPointF(7, 0), color, c.door.locked); } // Start / mid / end text labels, drawn in the line font and line-text color. if (m_scene->textVisible() && (!c.startText.isEmpty() || !c.midText.isEmpty() || !c.endText.isEmpty())) { const QFont lineFont = qfontFromSpec(map->settings.lineFont, 9.0); QColor textColor = map->settings.colors[ColorLineText]; if (!textColor.isValid()) textColor = QColor(Qt::black); const double off = map->settings.textOffset > 0.0 ? map->settings.textOffset : 4.0; const bool wrap = map->settings.wrapTextAtDashes; if (!c.startText.isEmpty()) { const QPointF a = m_points.first() + roomTypeLabelAdjust(map, c, true); const QPointF delta = m_points.first() - m_points.at(1); drawConnectionLabel(painter, a, delta, LabelAlign::Near, applyDashWrapping(c.startText, wrap), lineFont, textColor, off); } if (!c.endText.isEmpty()) { const QPointF a = m_points.last() + roomTypeLabelAdjust(map, c, false); const QPointF delta = m_points.last() - m_points.at(m_points.size() - 2); drawConnectionLabel(painter, a, delta, LabelAlign::Far, applyDashWrapping(c.endText, wrap), lineFont, textColor, off); } if (!c.midText.isEmpty()) { const MidInfo mid = midSegmentInfo(m_points); drawConnectionLabel(painter, mid.point, mid.unit, LabelAlign::Center, applyDashWrapping(c.midText, wrap), lineFont, textColor, off); } } // Editing handles at each vertex when selected: a solid dot for a docked // endpoint, a hollow dot for a free waypoint. if (isSelected()) { const QVector handles = vertexHandlePoints(); QList sorted = c.vertices; std::sort(sorted.begin(), sorted.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); for (int i = 0; i < handles.size(); ++i) { const bool docked = (i < sorted.size()) ? sorted.at(i).docked : true; painter->setPen(QPen(QColor(30, 120, 220), 1.5)); painter->setBrush(docked ? QBrush(QColor(30, 120, 220)) : QBrush(Qt::white)); painter->drawEllipse(handles.at(i), 4.0, 4.0); } } } QVector ConnectionItem::vertexHandlePoints() const { QVector pts; const Map *map = m_scene->document(); if (!map) return pts; const int idx = map->connectionIndex(m_connId); if (idx < 0) return pts; QList vertices = map->connections.at(idx).vertices; std::sort(vertices.begin(), vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); for (const Vertex &v : vertices) { if (v.docked) { if (const Room *room = map->roomById(v.roomId)) pts.append(MapScene::portPoint(*room, v.port)); else pts.append(v.point); } else { pts.append(v.point); } } return pts; } int ConnectionItem::vertexHandleAt(const QPointF &localPos) const { const QVector pts = vertexHandlePoints(); for (int i = 0; i < pts.size(); ++i) { const QPointF d = localPos - pts.at(i); if (std::hypot(d.x(), d.y()) <= 12.0) return i; } return -1; } void ConnectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (isSelected() && event->button() == Qt::LeftButton) { const int v = vertexHandleAt(event->pos()); if (v >= 0) { Map *map = m_scene->document(); const int idx = map ? map->connectionIndex(m_connId) : -1; if (idx >= 0) { m_dragVertex = v; m_dragBefore = map->connections.at(idx); event->accept(); return; } } } QGraphicsItem::mousePressEvent(event); } void ConnectionItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_dragVertex >= 0) { Map *map = m_scene->document(); const int idx = map ? map->connectionIndex(m_connId) : -1; if (idx >= 0 && m_dragVertex < map->connections.at(idx).vertices.size()) { Vertex &v = map->connections[idx].vertices[m_dragVertex]; v.docked = false; // free while dragging; may re-dock on release v.point = event->scenePos(); updateRoute(); } event->accept(); return; } QGraphicsItem::mouseMoveEvent(event); } void ConnectionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (m_dragVertex >= 0) { const int dragged = m_dragVertex; m_dragVertex = -1; Map *map = m_scene->document(); const int idx = map ? map->connectionIndex(m_connId) : -1; if (idx >= 0 && dragged < map->connections.at(idx).vertices.size()) { Connection &c = map->connections[idx]; Vertex &v = c.vertices[dragged]; const bool isEndpoint = (dragged == 0 || dragged == c.vertices.size() - 1); // Snap-to-element magnetism: dock onto a room that contains the drop // point, or one within the snap-to-element distance of it. int roomId = m_scene->roomIdAt(event->scenePos()); if (roomId < 0 && map->settings.snapToElementSize > 0.0) roomId = m_scene->roomNearestWithin(event->scenePos(), map->settings.snapToElementSize); if (isEndpoint && roomId >= 0) { if (const Room *r = map->roomById(roomId)) { v.docked = true; v.roomId = roomId; v.port = MapScene::nearestPort(*r, event->scenePos(), AppSettings::instance().portAdjustDetail); } } else { v.docked = false; v.point = m_scene->snap(event->scenePos()); } const Connection after = c; if (after != m_dragBefore) { if (m_scene->undoStack()) m_scene->undoStack()->push( new EditConnectionCommand(m_scene, m_connId, m_dragBefore, after)); } updateRoute(); } event->accept(); return; } QGraphicsItem::mouseReleaseEvent(event); } void ConnectionItem::addWaypointAt(const QPointF &scenePos) { Map *map = m_scene->document(); const int idx = map ? map->connectionIndex(m_connId) : -1; if (idx < 0) return; Connection before = map->connections.at(idx); Connection &c = map->connections[idx]; // Find the segment closest to the click and insert the new vertex into it. const QVector pts = vertexHandlePoints(); int insertAfter = 0; double best = 1e18; for (int i = 0; i + 1 < pts.size(); ++i) { const QPointF a = pts.at(i); const QPointF b = pts.at(i + 1); const QPointF ab = b - a; const double len2 = ab.x() * ab.x() + ab.y() * ab.y(); double t = 0.0; if (len2 > 1e-9) t = std::clamp(QPointF::dotProduct(scenePos - a, ab) / len2, 0.0, 1.0); const QPointF proj = a + ab * t; const QPointF d = scenePos - proj; const double dist = std::hypot(d.x(), d.y()); if (dist < best) { best = dist; insertAfter = i; } } Vertex nv; nv.docked = false; nv.point = scenePos; c.vertices.insert(insertAfter + 1, nv); for (int i = 0; i < c.vertices.size(); ++i) c.vertices[i].index = i; const Connection after = c; if (m_scene->undoStack()) m_scene->undoStack()->push(new EditConnectionCommand(m_scene, m_connId, before, after)); updateRoute(); } void ConnectionItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { m_scene->activateConnection(m_connId); event->accept(); } void ConnectionItem::clearHover() { if (m_hover) { m_hover = false; update(); } } void ConnectionItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { m_hover = true; update(); QGraphicsItem::hoverEnterEvent(event); } void ConnectionItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { m_hover = false; update(); QGraphicsItem::hoverLeaveEvent(event); } void ConnectionItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if (!isSelected()) { if (scene()) scene()->clearSelection(); setSelected(true); } QMenu menu; QAction *editAct = menu.addAction(QObject::tr("Edit…")); QAction *waypointAct = menu.addAction(QObject::tr("Add Waypoint Here")); QAction *deleteAct = menu.addAction(QObject::tr("Delete")); QAction *chosen = menu.exec(event->screenPos()); if (chosen == editAct) m_scene->activateConnection(m_connId); else if (chosen == waypointAct) addWaypointAt(event->scenePos()); else if (chosen == deleteAct) m_scene->deleteSelectionLater(); event->accept(); } } // namespace trizbort trizbort-qt-1.0.1/src/ConnectionItem.h000066400000000000000000000104071526521612000176720ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_CONNECTIONITEM_H #define TRIZBORT_CONNECTIONITEM_H #include #include #include "MapDocument.h" namespace trizbort { class MapScene; // A connection drawn as a polyline between its endpoints. Recomputes its route // from the document whenever an attached room moves or the model changes. class ConnectionItem : public QGraphicsItem { public: ConnectionItem(MapScene *scene, int connId); int connId() const { return m_connId; } void updateRoute(); // recompute geometry from the model const QVector &routePoints() const { return m_points; } void clearHover(); QRectF boundingRect() const override; QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; // Insert a free waypoint at a scene point (used from the context menu). void addWaypointAt(const QPointF &scenePos); protected: void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; private: // Scene-space points of the connection's vertices (endpoints + waypoints), // in index order; used to draw and hit-test the editing handles. QVector vertexHandlePoints() const; int vertexHandleAt(const QPointF &localPos) const; MapScene *m_scene; int m_connId; QVector m_points; int m_dragVertex = -1; // index of the vertex being dragged, or -1 Connection m_dragBefore; // snapshot for the undo command bool m_hover = false; // mouse hovering this connection }; } // namespace trizbort #endif // TRIZBORT_CONNECTIONITEM_H trizbort-qt-1.0.1/src/DotNetColor.cpp000066400000000000000000000132451526521612000175060ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #include "DotNetColor.h" namespace trizbort { namespace { // Auto-generated from the .NET KnownColor set; do not edit by hand. static const struct { unsigned int rgb; const char *name; } kKnownColors[] = { {0xF0F8FF, "AliceBlue"}, {0xFAEBD7, "AntiqueWhite"}, {0x00FFFF, "Aqua"}, {0x7FFFD4, "Aquamarine"}, {0xF0FFFF, "Azure"}, {0xF5F5DC, "Beige"}, {0xFFE4C4, "Bisque"}, {0x000000, "Black"}, {0xFFEBCD, "BlanchedAlmond"}, {0x0000FF, "Blue"}, {0x8A2BE2, "BlueViolet"}, {0xA52A2A, "Brown"}, {0xDEB887, "BurlyWood"}, {0x5F9EA0, "CadetBlue"}, {0x7FFF00, "Chartreuse"}, {0xD2691E, "Chocolate"}, {0xFF7F50, "Coral"}, {0x6495ED, "CornflowerBlue"}, {0xFFF8DC, "Cornsilk"}, {0xDC143C, "Crimson"}, {0x00008B, "DarkBlue"}, {0x008B8B, "DarkCyan"}, {0xB8860B, "DarkGoldenrod"}, {0xA9A9A9, "DarkGray"}, {0x006400, "DarkGreen"}, {0xBDB76B, "DarkKhaki"}, {0x8B008B, "DarkMagenta"}, {0x556B2F, "DarkOliveGreen"}, {0xFF8C00, "DarkOrange"}, {0x9932CC, "DarkOrchid"}, {0x8B0000, "DarkRed"}, {0xE9967A, "DarkSalmon"}, {0x8FBC8B, "DarkSeaGreen"}, {0x483D8B, "DarkSlateBlue"}, {0x2F4F4F, "DarkSlateGray"}, {0x00CED1, "DarkTurquoise"}, {0x9400D3, "DarkViolet"}, {0xFF1493, "DeepPink"}, {0x00BFFF, "DeepSkyBlue"}, {0x696969, "DimGray"}, {0x1E90FF, "DodgerBlue"}, {0xB22222, "Firebrick"}, {0xFFFAF0, "FloralWhite"}, {0x228B22, "ForestGreen"}, {0xFF00FF, "Fuchsia"}, {0xDCDCDC, "Gainsboro"}, {0xF8F8FF, "GhostWhite"}, {0xFFD700, "Gold"}, {0xDAA520, "Goldenrod"}, {0x808080, "Gray"}, {0x008000, "Green"}, {0xADFF2F, "GreenYellow"}, {0xF0FFF0, "Honeydew"}, {0xFF69B4, "HotPink"}, {0xCD5C5C, "IndianRed"}, {0x4B0082, "Indigo"}, {0xFFFFF0, "Ivory"}, {0xF0E68C, "Khaki"}, {0xE6E6FA, "Lavender"}, {0xFFF0F5, "LavenderBlush"}, {0x7CFC00, "LawnGreen"}, {0xFFFACD, "LemonChiffon"}, {0xADD8E6, "LightBlue"}, {0xF08080, "LightCoral"}, {0xE0FFFF, "LightCyan"}, {0xFAFAD2, "LightGoldenrodYellow"}, {0x90EE90, "LightGreen"}, {0xD3D3D3, "LightGrey"}, {0xFFB6C1, "LightPink"}, {0xFFA07A, "LightSalmon"}, {0x20B2AA, "LightSeaGreen"}, {0x87CEFA, "LightSkyBlue"}, {0x778899, "LightSlateGray"}, {0xB0C4DE, "LightSteelBlue"}, {0xFFFFE0, "LightYellow"}, {0x00FF00, "Lime"}, {0x32CD32, "LimeGreen"}, {0xFAF0E6, "Linen"}, {0x800000, "Maroon"}, {0x66CDAA, "MediumAquamarine"}, {0x0000CD, "MediumBlue"}, {0xBA55D3, "MediumOrchid"}, {0x9370DB, "MediumPurple"}, {0x3CB371, "MediumSeaGreen"}, {0x7B68EE, "MediumSlateBlue"}, {0x00FA9A, "MediumSpringGreen"}, {0x48D1CC, "MediumTurquoise"}, {0xC71585, "MediumVioletRed"}, {0x191970, "MidnightBlue"}, {0xF5FFFA, "MintCream"}, {0xFFE4E1, "MistyRose"}, {0xFFE4B5, "Moccasin"}, {0xFFDEAD, "NavajoWhite"}, {0x000080, "Navy"}, {0xFDF5E6, "OldLace"}, {0x808000, "Olive"}, {0x6B8E23, "OliveDrab"}, {0xFFA500, "Orange"}, {0xFF4500, "OrangeRed"}, {0xDA70D6, "Orchid"}, {0xEEE8AA, "PaleGoldenrod"}, {0x98FB98, "PaleGreen"}, {0xAFEEEE, "PaleTurquoise"}, {0xDB7093, "PaleVioletRed"}, {0xFFEFD5, "PapayaWhip"}, {0xFFDAB9, "PeachPuff"}, {0xCD853F, "Peru"}, {0xFFC0CB, "Pink"}, {0xDDA0DD, "Plum"}, {0xB0E0E6, "PowderBlue"}, {0x800080, "Purple"}, {0xFF0000, "Red"}, {0xBC8F8F, "RosyBrown"}, {0x4169E1, "RoyalBlue"}, {0x8B4513, "SaddleBrown"}, {0xFA8072, "Salmon"}, {0xF4A460, "SandyBrown"}, {0x2E8B57, "SeaGreen"}, {0xFFF5EE, "SeaShell"}, {0xA0522D, "Sienna"}, {0xC0C0C0, "Silver"}, {0x87CEEB, "SkyBlue"}, {0x6A5ACD, "SlateBlue"}, {0x708090, "SlateGray"}, {0xFFFAFA, "Snow"}, {0x00FF7F, "SpringGreen"}, {0x4682B4, "SteelBlue"}, {0xD2B48C, "Tan"}, {0x008080, "Teal"}, {0xD8BFD8, "Thistle"}, {0xFF6347, "Tomato"}, {0x40E0D0, "Turquoise"}, {0xEE82EE, "Violet"}, {0xF5DEB3, "Wheat"}, {0xFFFFFF, "White"}, {0xF5F5F5, "WhiteSmoke"}, {0xFFFF00, "Yellow"}, {0x9ACD32, "YellowGreen"}, }; } // namespace // Reproduces System.Drawing.ColorTranslator.ToHtml for the palette and region // colors: a color whose RGB matches a .NET known color is written by name // (e.g. "MidnightBlue"), otherwise as uppercase "#RRGGBB". An invalid color // yields the empty string, as ToHtml does for Color.Empty. QString dotNetColorToHtml(const QColor &c) { if (!c.isValid()) return QString(); const unsigned int rgb = ((c.red() & 0xFF) << 16) | ((c.green() & 0xFF) << 8) | (c.blue() & 0xFF); for (const auto &kc : kKnownColors) { if (kc.rgb == rgb) return QString::fromLatin1(kc.name); } return QStringLiteral("#%1").arg(rgb, 6, 16, QLatin1Char('0')).toUpper(); } } // namespace trizbort trizbort-qt-1.0.1/src/DotNetColor.h000066400000000000000000000024431526521612000171510ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #ifndef TRIZBORT_DOTNETCOLOR_H #define TRIZBORT_DOTNETCOLOR_H #include #include namespace trizbort { // System.Drawing.ColorTranslator.ToHtml: a known color by name, else #RRGGBB. // Used for the settings palette and region colors, which Trizbort serializes // this way. (Room/connection colors use SaveColor/ToHex instead — always hex.) QString dotNetColorToHtml(const QColor &c); } // namespace trizbort #endif // TRIZBORT_DOTNETCOLOR_H trizbort-qt-1.0.1/src/EditCommands.cpp000066400000000000000000000355641526521612000176710ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "EditCommands.h" #include #include "MapScene.h" namespace trizbort { // ---- AddRoomCommand ------------------------------------------------------- AddRoomCommand::AddRoomCommand(MapScene *scene, double x, double y) : m_scene(scene) { Map *map = scene->document(); m_room.id = map->nextRoomId(); m_room.seq = map->nextSeq(); m_room.name = map->settings.defaultRoomName; m_room.x = x; m_room.y = y; switch (static_cast(map->settings.defaultRoomShape)) { case RoomShape::RoundedCorners: m_room.roundedCorners = true; break; case RoomShape::Ellipse: m_room.ellipse = true; break; case RoomShape::Octagonal: m_room.octagonal = true; break; default: break; } setText(QObject::tr("Add Room")); } void AddRoomCommand::redo() { Map *map = m_scene->document(); map->rooms.append(m_room); map->reindex(); m_scene->rebuild(); m_scene->selectRoomItem(m_room.id); } void AddRoomCommand::undo() { m_scene->document()->removeRoom(m_room.id); m_scene->rebuild(); } // ---- DeleteElementsCommand ------------------------------------------------ DeleteElementsCommand::DeleteElementsCommand(MapScene *scene, const QList &roomIds, const QList &connIds) : m_scene(scene) { Map *map = scene->document(); // Every connection docked to a deleted room is removed too; collect the // union with the explicitly-selected connections. QList allConnIds = connIds; for (int rid : roomIds) { for (const Connection &c : map->connections) { bool docked = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == rid) { docked = true; break; } } if (docked && !allConnIds.contains(c.id)) allConnIds.append(c.id); } } for (int i = 0; i < map->connections.size(); ++i) { if (allConnIds.contains(map->connections.at(i).id)) m_conns.append({i, map->connections.at(i)}); } for (int i = 0; i < map->rooms.size(); ++i) { if (roomIds.contains(map->rooms.at(i).id)) m_rooms.append({i, map->rooms.at(i)}); } setText(QObject::tr("Delete")); } void DeleteElementsCommand::redo() { Map *map = m_scene->document(); for (const auto &e : m_conns) map->removeConnection(e.value.id); for (const auto &e : m_rooms) { for (int i = 0; i < map->rooms.size(); ++i) { if (map->rooms.at(i).id == e.value.id) { map->rooms.removeAt(i); break; } } } map->reindex(); m_scene->rebuild(); } void DeleteElementsCommand::undo() { Map *map = m_scene->document(); // Re-insert at original indices in ascending order (they were captured // ascending), reconstructing the original element order exactly. for (const auto &e : m_rooms) map->rooms.insert(qMin(e.index, map->rooms.size()), e.value); for (const auto &e : m_conns) map->connections.insert(qMin(e.index, map->connections.size()), e.value); map->reindex(); m_scene->rebuild(); } // ---- MoveRoomsCommand ----------------------------------------------------- MoveRoomsCommand::MoveRoomsCommand(MapScene *scene, const QList &moves) : m_scene(scene) , m_moves(moves) { setText(QObject::tr("Move")); } void MoveRoomsCommand::redo() { Map *map = m_scene->document(); for (const RoomMove &m : m_moves) { if (Room *r = map->roomById(m.id)) { r->x = m.newPos.x(); r->y = m.newPos.y(); m_scene->refreshRoom(m.id); } } } void MoveRoomsCommand::undo() { Map *map = m_scene->document(); for (const RoomMove &m : m_moves) { if (Room *r = map->roomById(m.id)) { r->x = m.oldPos.x(); r->y = m.oldPos.y(); m_scene->refreshRoom(m.id); } } } // ---- EditRoomCommand ------------------------------------------------------ EditRoomCommand::EditRoomCommand(MapScene *scene, int roomId, const Room &oldRoom, const Room &newRoom) : m_scene(scene) , m_id(roomId) , m_old(oldRoom) , m_new(newRoom) { setText(QObject::tr("Edit Room")); } void EditRoomCommand::ensureRegion(const QString &name) { Map *map = m_scene->document(); m_addedRegion = false; if (name.isEmpty() || name == kNoRegion || map->regionByName(name)) return; map->regions.append(Region{name, QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}); m_addedRegion = true; } void EditRoomCommand::redo() { Map *map = m_scene->document(); ensureRegion(m_new.region); if (Room *r = map->roomById(m_id)) *r = m_new; m_scene->refreshRoom(m_id); } void EditRoomCommand::undo() { Map *map = m_scene->document(); if (Room *r = map->roomById(m_id)) *r = m_old; if (m_addedRegion) { bool inUse = false; for (const Room &other : map->rooms) if (other.region == m_new.region) inUse = true; if (!inUse) { for (qsizetype i = map->regions.size() - 1; i >= 0; --i) if (map->regions.at(i).name == m_new.region) map->regions.removeAt(i); } m_addedRegion = false; } m_scene->refreshRoom(m_id); } // ---- AddConnectedRoomCommand ---------------------------------------------- namespace { QString oppositePort(const QString &p) { if (p == QLatin1String("n")) return QStringLiteral("s"); if (p == QLatin1String("s")) return QStringLiteral("n"); if (p == QLatin1String("e")) return QStringLiteral("w"); if (p == QLatin1String("w")) return QStringLiteral("e"); if (p == QLatin1String("ne")) return QStringLiteral("sw"); if (p == QLatin1String("sw")) return QStringLiteral("ne"); if (p == QLatin1String("nw")) return QStringLiteral("se"); if (p == QLatin1String("se")) return QStringLiteral("nw"); return QStringLiteral("n"); } // Unit step for a compass port in screen coordinates (+y is down). void directionVector(const QString &dir, int &dx, int &dy) { dx = (dir.contains(QLatin1Char('e'))) ? 1 : (dir.contains(QLatin1Char('w')) ? -1 : 0); dy = (dir.contains(QLatin1Char('s'))) ? 1 : (dir.contains(QLatin1Char('n')) ? -1 : 0); } } // namespace AddConnectedRoomCommand::AddConnectedRoomCommand(MapScene *scene, int fromRoomId, const QString &direction, const QString &startLabel, const QString &endLabel) : m_scene(scene) { Map *map = scene->document(); const Room *from = map->roomById(fromRoomId); if (!from) return; int dx = 0; int dy = 0; directionVector(direction, dx, dy); if (dx == 0 && dy == 0) return; const double newW = 96.0; const double newH = 64.0; double gap = map->settings.preferredDistanceBetweenRooms; if (gap <= 0) gap = 64.0; double nx = from->x; double ny = from->y; if (dx > 0) nx = from->x + from->w + gap; else if (dx < 0) nx = from->x - newW - gap; if (dy > 0) ny = from->y + from->h + gap; else if (dy < 0) ny = from->y - newH - gap; const QPointF snapped = scene->snap(QPointF(nx, ny)); m_room.id = map->nextRoomId(); m_room.seq = map->nextSeq(); m_room.name = map->settings.defaultRoomName; m_room.w = newW; m_room.h = newH; m_room.x = snapped.x(); m_room.y = snapped.y(); switch (static_cast(map->settings.defaultRoomShape)) { case RoomShape::RoundedCorners: m_room.roundedCorners = true; break; case RoomShape::Ellipse: m_room.ellipse = true; break; case RoomShape::Octagonal: m_room.octagonal = true; break; default: break; } m_conn.id = m_room.id + 1; m_conn.seq = m_room.seq + 1; Vertex a; a.index = 0; a.docked = true; a.roomId = fromRoomId; a.port = direction; Vertex b; b.index = 1; b.docked = true; b.roomId = m_room.id; b.port = oppositePort(direction); m_conn.vertices << a << b; m_conn.startText = startLabel; m_conn.endText = endLabel; m_valid = true; setText(QObject::tr("Add Room %1").arg(direction.toUpper())); } void AddConnectedRoomCommand::redo() { if (!m_valid) return; Map *map = m_scene->document(); map->rooms.append(m_room); map->connections.append(m_conn); map->reindex(); m_scene->rebuild(); m_scene->selectRoomItem(m_room.id); } void AddConnectedRoomCommand::undo() { if (!m_valid) return; Map *map = m_scene->document(); map->removeConnection(m_conn.id); map->removeRoom(m_room.id); m_scene->rebuild(); } // ---- AddConnectionCommand ------------------------------------------------- AddConnectionCommand::AddConnectionCommand(MapScene *scene, int fromId, const QString &portA, int toId, const QString &portB) : m_scene(scene) { Map *map = scene->document(); m_conn.id = map->nextConnectionId(); m_conn.seq = map->nextSeq(); Vertex a; a.index = 0; a.docked = true; a.roomId = fromId; a.port = portA; Vertex b; b.index = 1; b.docked = true; b.roomId = toId; b.port = portB; m_conn.vertices << a << b; m_conn.style = scene->newConnectionStyle(); m_conn.flow = scene->newConnectionFlow(); setText(QObject::tr("Add Connection")); } void AddConnectionCommand::redo() { m_scene->document()->connections.append(m_conn); m_scene->rebuild(); } void AddConnectionCommand::undo() { m_scene->document()->removeConnection(m_conn.id); m_scene->rebuild(); } // ---- EditConnectionCommand ------------------------------------------------ EditConnectionCommand::EditConnectionCommand(MapScene *scene, int connId, const Connection &oldC, const Connection &newC) : m_scene(scene) , m_id(connId) , m_old(oldC) , m_new(newC) { setText(QObject::tr("Edit Connection")); } void EditConnectionCommand::redo() { Map *map = m_scene->document(); const int idx = map->connectionIndex(m_id); if (idx >= 0) map->connections[idx] = m_new; m_scene->refreshConnection(m_id); } void EditConnectionCommand::undo() { Map *map = m_scene->document(); const int idx = map->connectionIndex(m_id); if (idx >= 0) map->connections[idx] = m_old; m_scene->refreshConnection(m_id); } // ---- EditMapInfoCommand --------------------------------------------------- EditMapInfoCommand::EditMapInfoCommand(MapScene *scene, const Info &oldInfo, const Info &newInfo) : m_scene(scene) , m_old(oldInfo) , m_new(newInfo) { setText(QObject::tr("Map Properties")); } void EditMapInfoCommand::apply(const Info &info) { Map *map = m_scene->document(); map->title = info.title; map->author = info.author; map->description = info.description; map->history = info.history; } void EditMapInfoCommand::redo() { apply(m_new); } void EditMapInfoCommand::undo() { apply(m_old); } // ---- ReplaceContentCommand ------------------------------------------------ ReplaceContentCommand::ReplaceContentCommand(MapScene *scene, const Content &oldContent, const Content &newContent, const QString &text) : m_scene(scene) , m_old(oldContent) , m_new(newContent) { setText(text); } void ReplaceContentCommand::apply(const Content &content) { Map *map = m_scene->document(); map->rooms = content.rooms; map->connections = content.connections; map->regions = content.regions; map->reindex(); m_scene->rebuild(); } void ReplaceContentCommand::redo() { apply(m_new); } void ReplaceContentCommand::undo() { apply(m_old); } // ---- EditSettingsCommand -------------------------------------------------- EditSettingsCommand::EditSettingsCommand(MapScene *scene, const MapSettings &oldS, const QList &oldRegions, const MapSettings &newS, const QList &newRegions) : m_scene(scene) , m_oldS(oldS) , m_newS(newS) , m_oldRegions(oldRegions) , m_newRegions(newRegions) { setText(QObject::tr("Map Settings")); } void EditSettingsCommand::apply(const MapSettings &s, const QList ®ions) { Map *map = m_scene->document(); map->settings = s; map->regions = regions; m_scene->rebuild(); } void EditSettingsCommand::redo() { apply(m_newS, m_newRegions); } void EditSettingsCommand::undo() { apply(m_oldS, m_oldRegions); } } // namespace trizbort trizbort-qt-1.0.1/src/EditCommands.h000066400000000000000000000157301526521612000173270ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EDITCOMMANDS_H #define TRIZBORT_EDITCOMMANDS_H #include #include #include #include "MapDocument.h" // Undoable editing operations. Each command mutates the scene's document and // refreshes the affected items, so both directions leave a consistent canvas. namespace trizbort { class MapScene; class AddRoomCommand : public QUndoCommand { public: AddRoomCommand(MapScene *scene, double x, double y); void redo() override; void undo() override; int roomId() const { return m_room.id; } private: MapScene *m_scene; Room m_room; }; class DeleteElementsCommand : public QUndoCommand { public: DeleteElementsCommand(MapScene *scene, const QList &roomIds, const QList &connIds); void redo() override; void undo() override; bool isEmpty() const { return m_rooms.isEmpty() && m_conns.isEmpty(); } private: template struct Entry { int index; T value; }; MapScene *m_scene; QList> m_rooms; QList> m_conns; }; struct RoomMove { int id; QPointF oldPos; QPointF newPos; }; class MoveRoomsCommand : public QUndoCommand { public: MoveRoomsCommand(MapScene *scene, const QList &moves); void redo() override; void undo() override; private: MapScene *m_scene; QList m_moves; }; class EditRoomCommand : public QUndoCommand { public: EditRoomCommand(MapScene *scene, int roomId, const Room &oldRoom, const Room &newRoom); void redo() override; void undo() override; private: void ensureRegion(const QString &name); MapScene *m_scene; int m_id; Room m_old; Room m_new; bool m_addedRegion = false; }; // Automap primitive: create a new room one grid step away from an existing room // in a compass direction, and connect the two. A single undoable step. class AddConnectedRoomCommand : public QUndoCommand { public: AddConnectedRoomCommand(MapScene *scene, int fromRoomId, const QString &direction, const QString &startLabel = QString(), const QString &endLabel = QString()); void redo() override; void undo() override; bool valid() const { return m_valid; } int newRoomId() const { return m_room.id; } private: MapScene *m_scene; Room m_room; Connection m_conn; bool m_valid = false; }; class AddConnectionCommand : public QUndoCommand { public: AddConnectionCommand(MapScene *scene, int fromId, const QString &portA, int toId, const QString &portB); void redo() override; void undo() override; private: MapScene *m_scene; Connection m_conn; }; class EditConnectionCommand : public QUndoCommand { public: EditConnectionCommand(MapScene *scene, int connId, const Connection &oldC, const Connection &newC); void redo() override; void undo() override; private: MapScene *m_scene; int m_id; Connection m_old; Connection m_new; }; // Title/author/description/history live on the map, not on any item. class EditMapInfoCommand : public QUndoCommand { public: struct Info { QString title; QString author; QString description; QString history; }; EditMapInfoCommand(MapScene *scene, const Info &oldInfo, const Info &newInfo); void redo() override; void undo() override; private: void apply(const Info &info); MapScene *m_scene; Info m_old; Info m_new; }; // Replace the whole map content (rooms, connections, regions) as one step. // Used by bulk operations such as transcript import. class ReplaceContentCommand : public QUndoCommand { public: struct Content { QList rooms; QList connections; QList regions; }; ReplaceContentCommand(MapScene *scene, const Content &oldContent, const Content &newContent, const QString &text); void redo() override; void undo() override; private: void apply(const Content &content); MapScene *m_scene; Content m_old; Content m_new; }; // The full settings block (palette, fonts, grid, geometry) plus the region list. class EditSettingsCommand : public QUndoCommand { public: EditSettingsCommand(MapScene *scene, const MapSettings &oldS, const QList &oldRegions, const MapSettings &newS, const QList &newRegions); void redo() override; void undo() override; private: void apply(const MapSettings &s, const QList ®ions); MapScene *m_scene; MapSettings m_oldS; MapSettings m_newS; QList m_oldRegions; QList m_newRegions; }; } // namespace trizbort #endif // TRIZBORT_EDITCOMMANDS_H trizbort-qt-1.0.1/src/FontUtil.h000066400000000000000000000055501526521612000165230ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #ifndef TRIZBORT_FONTUTIL_H #define TRIZBORT_FONTUTIL_H #include #include "AppSettings.h" #include "MapDocument.h" namespace trizbort { // Build a QFont from a stored FontSpec, applying the family, size and style // flags. When the spec carries no family or a non-positive size (an unspecified // font), the given fallback size and the default UI family are used instead. // The .trizbort format stores C# GDI font sizes in GraphicsUnit.World, i.e. map // units (== pixels at 1:1), so the size is applied with setPixelSize; using // point sizes would render text ~33% larger and overflow small rooms. inline QFont qfontFromSpec(const FontSpec &spec, double fallbackSize) { QFont font; if (!spec.family.isEmpty()) font.setFamily(spec.family); else if (!AppSettings::instance().defaultFontName.isEmpty()) font.setFamily(AppSettings::instance().defaultFontName); // the app-wide default family font.setPixelSize(qRound(spec.size > 0.0 ? spec.size : fallbackSize)); font.setBold(spec.bold); font.setItalic(spec.italic); font.setUnderline(spec.underline); font.setStrikeOut(spec.strikeout); return font; } // Honor the map's "wrap text at dashes" setting when laying out canvas text, // mirroring TextBlock.cs. Qt's word wrap only guarantees breaks at whitespace, // so this rewrites the break behavior around hyphen-minus while keeping the // exact glyph: with wrapping on, a zero-width space (U+200B) after each hyphen // adds a break opportunity there; with it off, a word joiner (U+2060) removes // one. Like the C# original, this only applies to single-line text (text with // explicit newlines is drawn as authored). inline QString applyDashWrapping(const QString &text, bool wrapAtDashes) { if (!text.contains(QLatin1Char('-')) || text.contains(QLatin1Char('\n'))) return text; QString out = text; out.replace(QLatin1Char('-'), wrapAtDashes ? QStringLiteral("-​") : QStringLiteral("-â� ")); return out; } } // namespace trizbort #endif // TRIZBORT_FONTUTIL_H trizbort-qt-1.0.1/src/HelpWindow.cpp000066400000000000000000000050541526521612000173710ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #include "HelpWindow.h" #include #include #include #include #include #include namespace trizbort { HelpWindow::HelpWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(tr("Trizbort-qt Help")); resize(800, 600); m_browser = new QTextBrowser(this); // The pages are self-contained; any (rare) external link opens in a browser. m_browser->setOpenExternalLinks(true); // Apply the bundled stylesheet as the document's default style sheet, so it // styles every page the browser loads. Must be set before the first source. QFile css(QStringLiteral(":/help/help.css")); if (css.open(QIODevice::ReadOnly | QIODevice::Text)) m_browser->document()->setDefaultStyleSheet(QString::fromUtf8(css.readAll())); setCentralWidget(m_browser); QToolBar *nav = addToolBar(tr("Navigation")); nav->setMovable(false); QAction *back = nav->addAction(tr("Back")); back->setShortcut(QKeySequence::Back); QAction *forward = nav->addAction(tr("Forward")); forward->setShortcut(QKeySequence::Forward); QAction *contents = nav->addAction(tr("Contents")); connect(back, &QAction::triggered, m_browser, &QTextBrowser::backward); connect(forward, &QAction::triggered, m_browser, &QTextBrowser::forward); connect(contents, &QAction::triggered, m_browser, &QTextBrowser::home); back->setEnabled(false); forward->setEnabled(false); connect(m_browser, &QTextBrowser::backwardAvailable, back, &QAction::setEnabled); connect(m_browser, &QTextBrowser::forwardAvailable, forward, &QAction::setEnabled); m_browser->setSource(QUrl(QStringLiteral("qrc:/help/index.html"))); } } // namespace trizbort trizbort-qt-1.0.1/src/HelpWindow.h000066400000000000000000000024611526521612000170350ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * The built-in help window: a QTextBrowser over the help pages bundled from * help/ via help.qrc. See help/CONVENTIONS.md. */ #ifndef TRIZBORT_HELPWINDOW_H #define TRIZBORT_HELPWINDOW_H #include class QTextBrowser; namespace trizbort { class HelpWindow : public QMainWindow { Q_OBJECT public: explicit HelpWindow(QWidget *parent = nullptr); private: QTextBrowser *m_browser = nullptr; }; } // namespace trizbort #endif // TRIZBORT_HELPWINDOW_H trizbort-qt-1.0.1/src/LabelGeometry.h000066400000000000000000000065771526521612000175240ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_LABELGEOMETRY_H #define TRIZBORT_LABELGEOMETRY_H #include #include #include #include "MapDocument.h" // kPi namespace trizbort { inline bool inRange(double value, double min, double max) { if (min > max) return value >= max && value <= min; return value >= min && value <= max; } inline QString labelCompassFromDelta(const QPointF &delta, double &angleDeg) { angleDeg = -(std::atan2(delta.y(), delta.x()) * 180.0 / kPi); if (inRange(angleDeg, 0, 45)) return QStringLiteral("nw"); if (inRange(angleDeg, 45, 90)) return QStringLiteral("se"); if (inRange(angleDeg, 90, 135)) return QStringLiteral("sw"); if (inRange(angleDeg, 135, 180)) return QStringLiteral("ne"); if (inRange(angleDeg, 0, -45)) return QStringLiteral("ne"); if (inRange(angleDeg, -45, -90)) return QStringLiteral("ne"); if (inRange(angleDeg, -90, -135)) return QStringLiteral("nw"); if (inRange(angleDeg, -135, -180)) return QStringLiteral("se"); return QStringLiteral("e"); } } // namespace trizbort #endif // TRIZBORT_LABELGEOMETRY_H trizbort-qt-1.0.1/src/MainWindow.cpp000066400000000000000000003331211526521612000173640ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "AppSettings.h" #include "AutomapGui.h" #include "ConnectionDialog.h" #include "EditCommands.h" #include "HelpWindow.h" #include "MapRender.h" #include "MapScene.h" #include "MapStatisticsDialog.h" #include "MapView.h" #include "MinimapView.h" #include "QuickFindDialog.h" #include "RoomDialog.h" #include "SettingsDialog.h" #include "TranscriptAutomapper.h" #include "TrizbortReader.h" #include "TrizbortWriter.h" #include "export/CodeExporter.h" #include "export/ExporterFactory.h" namespace trizbort { MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_scene = new MapScene(this); m_view = new MapView(this); m_view->setScene(m_scene); // Intercept canvas key presses (arrows, keypad) for the C# keyboard-editing // and navigation model before the view scrolls. m_view->installEventFilter(this); setCentralWidget(m_view); // Minimap overview in a dockable panel (hidden until the user shows it). m_minimapDock = new QDockWidget(tr("Mini Map"), this); m_minimapDock->setWidget(new MinimapView(m_scene, m_view, m_minimapDock)); m_minimapDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, m_minimapDock); m_minimapDock->hide(); m_scene->setUndoStack(&m_undo); connect(m_scene, &MapScene::editRoomRequested, this, &MainWindow::editRoom, Qt::QueuedConnection); connect(m_scene, &MapScene::editConnectionRequested, this, &MainWindow::editConnection, Qt::QueuedConnection); connect(m_scene, &MapScene::canvasContextMenuRequested, this, &MainWindow::showCanvasContextMenu, Qt::QueuedConnection); connect(m_scene, &MapScene::selectionSummary, this, [this](const QString &text) { statusBar()->showMessage(text); }); connect(&m_undo, &QUndoStack::cleanChanged, this, [this](bool clean) { setWindowModified(!clean); }); createActions(); // A permanent zoom-percentage indicator in the status bar, kept current as // the view is zoomed (a scale change alters the scrollbar range). m_zoomLabel = new QLabel(this); statusBar()->addPermanentWidget(m_zoomLabel); m_automapStopButton = new QPushButton(tr("Stop Automapping"), this); m_automapStopButton->setVisible(false); connect(m_automapStopButton, &QPushButton::clicked, this, &MainWindow::stopLiveAutomap); statusBar()->addPermanentWidget(m_automapStopButton); auto updateZoom = [this] { m_zoomLabel->setText(tr("%1%").arg(qRound(m_view->transform().m11() * 100.0))); }; connect(m_view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, updateZoom); connect(m_view->verticalScrollBar(), &QScrollBar::rangeChanged, this, updateZoom); updateZoom(); // Restore preferences (window geometry, recent files, toggles). loadPreferences(); m_view->setInvertWheelZoom(m_invertWheel); rebuildRecentMenu(); // Start with a fresh, empty document, then optionally reopen the last map. newFile(); if (m_loadLastOnStart && !m_recentFiles.isEmpty() && QFile::exists(m_recentFiles.first())) loadFile(m_recentFiles.first()); statusBar()->showMessage(tr("Ready. Insert adds a room; C toggles connect mode.")); } void MainWindow::createActions() { QMenu *fileMenu = menuBar()->addMenu(tr("&File")); QToolBar *toolBar = addToolBar(tr("Main")); fileMenu->addAction(tr("&New"), QKeySequence::New, this, &MainWindow::newFile); fileMenu->addAction(tr("&Open…"), QKeySequence::Open, this, &MainWindow::openFile); fileMenu->addAction(tr("Open from &URL…"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_O), this, &MainWindow::openFromUrl); fileMenu->addAction(tr("&Save"), QKeySequence::Save, this, [this] { save(); }); fileMenu->addAction(tr("Save &As…"), QKeySequence::SaveAs, this, [this] { saveAs(); }); m_recentMenu = fileMenu->addMenu(tr("Recent &Maps")); fileMenu->addSeparator(); fileMenu->addAction(tr("Bac&kup"), QKeySequence(Qt::CTRL | Qt::Key_B), this, &MainWindow::backupMap); fileMenu->addAction(tr("S&mart Save"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_S), this, &MainWindow::smartSave); fileMenu->addSeparator(); QMenu *exportMenu = fileMenu->addMenu(tr("&Export")); for (const ExportFormat &fmt : exportFormats()) { const QString key = fmt.key; exportMenu->addAction(fmt.label, this, [this, key] { exportMap(key); }); } exportMenu->addSeparator(); exportMenu->addAction(tr("PDF…"), this, &MainWindow::exportPdf); exportMenu->addAction(tr("Image (PNG/JPEG/BMP)…"), this, &MainWindow::exportImage); QMenu *clipboardMenu = fileMenu->addMenu(tr("Export to &Clipboard")); for (const ExportFormat &fmt : exportFormats()) { const QString key = fmt.key; clipboardMenu->addAction(fmt.label, this, [this, key] { exportToClipboard(key); }); } fileMenu->addSeparator(); fileMenu->addAction(tr("&Import Transcript…"), this, &MainWindow::importTranscript); fileMenu->addSeparator(); fileMenu->addAction(tr("&Quit"), QKeySequence::Quit, this, &QWidget::close); QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); QAction *undoAct = m_undo.createUndoAction(this, tr("&Undo")); undoAct->setShortcut(QKeySequence::Undo); QAction *redoAct = m_undo.createRedoAction(this, tr("&Redo")); redoAct->setShortcut(QKeySequence::Redo); editMenu->addAction(undoAct); editMenu->addAction(redoAct); editMenu->addSeparator(); editMenu->addAction(tr("Add &Room"), QKeySequence(Qt::Key_Insert), this, &MainWindow::addRoom); editMenu->addAction(tr("&Delete Selection"), QKeySequence::Delete, this, &MainWindow::deleteSelection); m_connectAction = editMenu->addAction(tr("&Connect Mode")); m_connectAction->setCheckable(true); m_connectAction->setShortcut(QKeySequence(Qt::Key_C)); connect(m_connectAction, &QAction::toggled, this, &MainWindow::toggleConnectMode); editMenu->addSeparator(); editMenu->addAction(tr("Cu&t"), QKeySequence::Cut, this, [this] { copySelection(); deleteSelection(); }); editMenu->addAction(tr("&Copy"), QKeySequence::Copy, this, &MainWindow::copySelection); editMenu->addAction(tr("&Paste"), QKeySequence::Paste, this, &MainWindow::paste); editMenu->addAction(tr("Copy Colo&r"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C), this, &MainWindow::copyColor); editMenu->addAction(tr("Paste Colo&r"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_V), this, &MainWindow::pasteColor); editMenu->addSeparator(); editMenu->addAction(tr("Select &All"), QKeySequence::SelectAll, this, [this] { m_scene->selectAll(); }); editMenu->addAction(tr("Select &None"), QKeySequence(Qt::Key_Escape), this, [this] { m_scene->clearSelection(); }); editMenu->addAction(tr("&Find Room…"), QKeySequence::Find, this, [this] { QuickFindDialog dlg(m_map, this); if (dlg.exec() != QDialog::Accepted) return; const int id = dlg.selectedRoomId(); if (id < 0) return; m_scene->selectRoomItem(id); if (const Room *r = m_map.roomById(id)) m_view->centerOn(r->x + r->w / 2.0, r->y + r->h / 2.0); }); QMenu *selectMenu = editMenu->addMenu(tr("Se&lect")); selectMenu->addAction(tr("Unconnected Rooms"), this, [this] { selectSpecial(SelectKind::UnconnectedRooms); }); selectMenu->addAction(tr("Rooms With Objects"), this, [this] { selectSpecial(SelectKind::RoomsWithObjects); }); selectMenu->addAction(tr("Rooms Without Objects"), this, [this] { selectSpecial(SelectKind::RoomsWithoutObjects); }); selectMenu->addAction(tr("Rooms in Selected &Regions"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A), this, &MainWindow::selectRoomsInSelectedRegions); selectMenu->addSeparator(); selectMenu->addAction(tr("All Connections"), this, [this] { selectSpecial(SelectKind::AllConnections); }); selectMenu->addAction(tr("Dangling Connections"), this, [this] { selectSpecial(SelectKind::DanglingConnections); }); selectMenu->addAction(tr("Self-Looping Connections"), this, [this] { selectSpecial(SelectKind::SelfLoopingConnections); }); // Automap: create a connected room in a compass direction from the selection. QMenu *automapMenu = editMenu->addMenu(tr("Add Connec&ted Room")); struct Dir { const char *label; const char *dir; QKeySequence shortcut; }; // These items add a room in a direction (click). The arrow chords are // reserved for the keyboard-navigation model in eventFilter: Ctrl+arrow // navigates-or-adds and Ctrl+Alt+arrow resizes, matching the C# canvas. const Dir dirs[] = { {"North", "n", QKeySequence()}, {"South", "s", QKeySequence()}, {"East", "e", QKeySequence()}, {"West", "w", QKeySequence()}, {"North-East", "ne", QKeySequence()}, {"North-West", "nw", QKeySequence()}, {"South-East", "se", QKeySequence()}, {"South-West", "sw", QKeySequence()}, }; for (const Dir &d : dirs) { const QString dir = QString::fromLatin1(d.dir); QAction *act = automapMenu->addAction(tr(d.label), this, [this, dir] { addConnectedRoom(dir); }); if (!d.shortcut.isEmpty()) act->setShortcut(d.shortcut); } automapMenu->addSeparator(); // Up/Down/In/Out place the room in a cardinal direction and label the // connection, mirroring the C# up/down/in/out mapping. automapMenu->addAction(tr("Up"), this, [this] { addConnectedRoomLabeled("n", "up", "down", tr("Up")); }); automapMenu->addAction(tr("Down"), this, [this] { addConnectedRoomLabeled("s", "down", "up", tr("Down")); }); automapMenu->addAction(tr("In"), this, [this] { addConnectedRoomLabeled("e", "in", "out", tr("In")); }); automapMenu->addAction(tr("Out"), this, [this] { addConnectedRoomLabeled("w", "out", "in", tr("Out")); }); editMenu->addSeparator(); editMenu->addAction(tr("&Map Properties…"), this, &MainWindow::editMapProperties); editMenu->addAction(tr("Map &Settings…"), this, &MainWindow::editMapSettings); // --- Rooms menu --- QMenu *roomsMenu = menuBar()->addMenu(tr("&Rooms")); roomsMenu->addAction(tr("Re&name…"), QKeySequence(Qt::Key_F2), this, &MainWindow::renameSelectedRoom); roomsMenu->addAction(tr("Change &Region…"), QKeySequence(Qt::SHIFT | Qt::Key_F2), this, &MainWindow::changeSelectedRegion); roomsMenu->addSeparator(); roomsMenu->addAction(tr("Toggle &Darkness"), QKeySequence(Qt::Key_K), this, [this] { applyToSelectedRooms(tr("Toggle Darkness"), [](Room &r) { r.isDark = !r.isDark; }); }); roomsMenu->addAction(tr("Force Dar&k"), QKeySequence(Qt::CTRL | Qt::Key_K), this, [this] { applyToSelectedRooms(tr("Force Dark"), [](Room &r) { r.isDark = true; }); }); roomsMenu->addAction(tr("Force &Lighted"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_K), this, [this] { applyToSelectedRooms(tr("Force Lighted"), [](Room &r) { r.isDark = false; }); }); roomsMenu->addSeparator(); roomsMenu->addAction(tr("Set as &Start Room"), QKeySequence(Qt::CTRL | Qt::Key_F5), this, [this] { setStartOrEndRoom(true); }); roomsMenu->addAction(tr("Set as &End Room"), QKeySequence(Qt::SHIFT | Qt::Key_F5), this, [this] { setStartOrEndRoom(false); }); roomsMenu->addSeparator(); QMenu *shapeMenu = roomsMenu->addMenu(tr("S&hape")); shapeMenu->addAction(tr("S&quare"), QKeySequence(Qt::CTRL | Qt::Key_H), this, [this] { setSelectedRoomShape(RoomShape::SquareCorners); }); shapeMenu->addAction(tr("&Rounded"), QKeySequence(Qt::CTRL | Qt::Key_R), this, [this] { setSelectedRoomShape(RoomShape::RoundedCorners); }); shapeMenu->addAction(tr("&Ellipse"), QKeySequence(Qt::CTRL | Qt::Key_E), this, [this] { setSelectedRoomShape(RoomShape::Ellipse); }); shapeMenu->addAction(tr("&Octagonal"), QKeySequence(Qt::CTRL | Qt::Key_8), this, [this] { setSelectedRoomShape(RoomShape::Octagonal); }); roomsMenu->addSeparator(); roomsMenu->addAction(tr("Bring to &Front"), this, [this] { setSelectedRoomsZOrder(true); }); roomsMenu->addAction(tr("Send to &Back"), this, [this] { setSelectedRoomsZOrder(false); }); roomsMenu->addSeparator(); roomsMenu->addAction(tr("&Join Rooms"), QKeySequence(Qt::Key_J), this, &MainWindow::joinSelectedRooms); QMenu *swapMenu = roomsMenu->addMenu(tr("S&wap (two rooms)")); swapMenu->addAction(tr("Swap &Objects"), QKeySequence(Qt::Key_W), this, [this] { swapSelectedRooms(SwapMode::Objects); }); swapMenu->addAction(tr("Swap &Names"), QKeySequence(Qt::CTRL | Qt::Key_W), this, [this] { swapSelectedRooms(SwapMode::Names); }); swapMenu->addAction(tr("Swap &Formats"), QKeySequence(Qt::SHIFT | Qt::Key_W), this, [this] { swapSelectedRooms(SwapMode::Formats); }); swapMenu->addAction(tr("Swap &Regions"), QKeySequence(Qt::ALT | Qt::Key_W), this, [this] { swapSelectedRooms(SwapMode::Regions); }); // --- Connections menu --- QMenu *connMenu = menuBar()->addMenu(tr("&Connections")); connMenu->addAction(tr("Re&verse"), QKeySequence(Qt::Key_V), this, &MainWindow::reverseSelectedConnections); connMenu->addAction(tr("Rotate &Source Port"), QKeySequence(Qt::Key_BracketLeft), this, [this] { rotateSelectedConnectors(true); }); connMenu->addAction(tr("Rotate &Target Port"), QKeySequence(Qt::Key_BracketRight), this, [this] { rotateSelectedConnectors(false); }); connMenu->addAction(tr("&Insert Room on Connection"), this, &MainWindow::insertRoomOnConnection); connMenu->addAction(tr("Make Ortho&gonal"), this, &MainWindow::makeSelectedConnectionsOrthogonal); connMenu->addAction(tr("Delete Room's &Connections"), QKeySequence(Qt::CTRL | Qt::Key_D), this, &MainWindow::deleteConnectionsOfSelectedRoom); QMenu *styleMenu = connMenu->addMenu(tr("&Line Style")); styleMenu->addAction(tr("&Solid"), this, [this] { applyToSelectedConnections(tr("Solid Line"), [](Connection &c) { c.style = ConnectionStyle::Solid; }); }); styleMenu->addAction(tr("&Dashed"), QKeySequence(Qt::Key_T), this, [this] { applyToSelectedConnections(tr("Dashed Line"), [](Connection &c) { c.style = ConnectionStyle::Dashed; }); }); QMenu *flowMenu = connMenu->addMenu(tr("&Flow")); flowMenu->addAction(tr("&Two-way"), this, [this] { applyToSelectedConnections(tr("Two-way"), [](Connection &c) { c.flow = ConnectionFlow::TwoWay; }); }); flowMenu->addAction(tr("One-&way"), QKeySequence(Qt::Key_A), this, [this] { applyToSelectedConnections(tr("One-way"), [](Connection &c) { c.flow = ConnectionFlow::OneWay; }); }); QMenu *labelMenu = connMenu->addMenu(tr("La&bel")); const struct { const char *label; Qt::Key key; const char *start; const char *end; } kLabels[] = {{"Up / Down", Qt::Key_U, "up", "down"}, {"Down / Up", Qt::Key_D, "down", "up"}, {"In / Out", Qt::Key_I, "in", "out"}, {"Out / In", Qt::Key_O, "out", "in"}}; for (const auto &l : kLabels) { const QString s = QString::fromLatin1(l.start); const QString e = QString::fromLatin1(l.end); labelMenu->addAction(tr(l.label), QKeySequence(l.key), this, [this, s, e] { applyToSelectedConnections(tr("Set Label"), [s, e](Connection &c) { c.startText = s; c.endText = e; }); }); } connMenu->addAction(tr("&Plain"), QKeySequence(Qt::Key_P), this, [this] { applyToSelectedConnections(tr("Plain Connection"), [](Connection &c) { c.style = ConnectionStyle::Solid; c.flow = ConnectionFlow::TwoWay; c.startText.clear(); c.midText.clear(); c.endText.clear(); }); }); connMenu->addSeparator(); QMenu *newConnMenu = connMenu->addMenu(tr("&New Connection Defaults")); auto *newDashed = newConnMenu->addAction(tr("Dashed")); newDashed->setCheckable(true); connect(newDashed, &QAction::toggled, this, [this](bool on) { m_scene->setNewConnectionStyle(on ? ConnectionStyle::Dashed : ConnectionStyle::Solid); }); auto *newOneWay = newConnMenu->addAction(tr("One-way")); newOneWay->setCheckable(true); connect(newOneWay, &QAction::toggled, this, [this](bool on) { m_scene->setNewConnectionFlow(on ? ConnectionFlow::OneWay : ConnectionFlow::TwoWay); }); // --- Automap menu --- QMenu *automapTopMenu = menuBar()->addMenu(tr("&Automap")); automapTopMenu->addAction(tr("&Start Live Automap…"), this, &MainWindow::startLiveAutomap); m_automapStopAction = automapTopMenu->addAction(tr("S&top Automapping"), this, &MainWindow::stopLiveAutomap); m_automapStopAction->setEnabled(false); m_automapStepAction = automapTopMenu->addAction(tr("Ste&p"), QKeySequence(Qt::Key_F11), this, [this] { if (m_automapController) m_automapController->proceed(); }); m_automapStepAction->setEnabled(false); m_automapRunAction = automapTopMenu->addAction( tr("&Run to Completion"), QKeySequence(Qt::SHIFT | Qt::Key_F11), this, [this] { if (m_automapController) m_automapController->runToCompletion(); }); m_automapRunAction->setEnabled(false); automapTopMenu->addSeparator(); automapTopMenu->addAction(tr("&Import Transcript (one-shot)…"), this, &MainWindow::importTranscript); // --- Validation menu --- QMenu *validationMenu = menuBar()->addMenu(tr("&Validation")); auto *vUnique = validationMenu->addAction(tr("Rooms Must Have a &Unique Name")); auto *vDesc = validationMenu->addAction(tr("Rooms Must Have a &Description")); auto *vSub = validationMenu->addAction(tr("Rooms Must Have a &Subtitle")); auto *vDang = validationMenu->addAction(tr("Rooms Must Not Have a Dan&gling Connection")); for (QAction *a : {vUnique, vDesc, vSub, vDang}) a->setCheckable(true); auto applyValidation = [this, vUnique, vDesc, vSub, vDang] { MapScene::ValidationFlags f; f.uniqueNames = vUnique->isChecked(); f.description = vDesc->isChecked(); f.subtitle = vSub->isChecked(); f.noDangling = vDang->isChecked(); m_scene->setValidation(f); }; for (QAction *a : {vUnique, vDesc, vSub, vDang}) connect(a, &QAction::triggered, this, applyValidation); // --- Tools menu --- QMenu *toolsMenu = menuBar()->addMenu(tr("&Tools")); toolsMenu->addAction(tr("Map St&atistics…"), this, [this] { MapStatisticsDialog dlg(m_map, this); dlg.exec(); }); toolsMenu->addSeparator(); toolsMenu->addAction(tr("&Restore Default Map Settings"), this, [this] { if (QMessageBox::question(this, tr("Restore Defaults"), tr("Reset all map appearance settings to their defaults?")) != QMessageBox::Yes) return; m_undo.push(new EditSettingsCommand(m_scene, m_map.settings, m_map.regions, MapSettings(), m_map.regions)); }); toolsMenu->addSeparator(); toolsMenu->addAction(tr("&Application Settings…"), this, &MainWindow::showAppSettings); QMenu *viewMenu = menuBar()->addMenu(tr("&View")); viewMenu->addAction(tr("Zoom &In"), QKeySequence::ZoomIn, m_view, &MapView::zoomIn); viewMenu->addAction(tr("Zoom &Out"), QKeySequence::ZoomOut, m_view, &MapView::zoomOut); viewMenu->addAction(tr("Zoom In &1%"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Equal), this, [this] { m_view->microZoom(true); }); viewMenu->addAction(tr("Zoom Out 1&%"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Minus), this, [this] { m_view->microZoom(false); }); QMenu *zoomMenu = viewMenu->addMenu(tr("&Zoom To")); zoomMenu->addAction(tr("50%"), this, [this] { m_view->setZoomPercent(50); }); zoomMenu->addAction(tr("100%"), this, [this] { m_view->setZoomPercent(100); }); zoomMenu->addAction(tr("200%"), this, [this] { m_view->setZoomPercent(200); }); viewMenu->addAction(tr("&Reset Zoom"), QKeySequence(Qt::CTRL | Qt::Key_0), m_view, &MapView::resetZoom); viewMenu->addAction(tr("Reset &Origin"), QKeySequence(Qt::Key_Home), m_view, &MapView::resetOrigin); viewMenu->addAction(tr("&Fit to Window"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_F), m_view, &MapView::zoomToFit); viewMenu->addSeparator(); m_gridAction = viewMenu->addAction(tr("Show &Grid")); QAction *gridAct = m_gridAction; gridAct->setCheckable(true); gridAct->setChecked(m_map.settings.gridVisible); connect(gridAct, &QAction::triggered, this, [this](bool on) { MapSettings ns = m_map.settings; ns.gridVisible = on; m_undo.push(new EditSettingsCommand(m_scene, m_map.settings, m_map.regions, ns, m_map.regions)); }); m_snapAction = viewMenu->addAction(tr("&Snap to Grid")); QAction *snapAct = m_snapAction; snapAct->setCheckable(true); snapAct->setChecked(m_map.settings.snapToGrid); connect(snapAct, &QAction::triggered, this, [this](bool on) { MapSettings ns = m_map.settings; ns.snapToGrid = on; m_undo.push(new EditSettingsCommand(m_scene, m_map.settings, m_map.regions, ns, m_map.regions)); }); viewMenu->addSeparator(); auto *textAct = viewMenu->addAction(tr("Show &Text")); textAct->setCheckable(true); textAct->setChecked(true); textAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_F4)); connect(textAct, &QAction::triggered, this, [this](bool on) { m_scene->setTextVisible(on); }); if (m_minimapDock) { QAction *mm = m_minimapDock->toggleViewAction(); mm->setText(tr("Mini &Map")); viewMenu->addAction(mm); } // --- Help menu --- QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(tr("&Help Contents"), QKeySequence::HelpContents, this, &MainWindow::showHelp); helpMenu->addSeparator(); helpMenu->addAction(tr("&About Trizbort-qt…"), this, [this] { // A structured About dialog (rather than a bare message box), mirroring // the C# AboutDialog: title, version, description, credits and a link. QDialog dialog(this); dialog.setWindowTitle(tr("About Trizbort-qt")); auto *layout = new QVBoxLayout(&dialog); auto *heading = new QLabel(tr("

Trizbort-qt

"), &dialog); heading->setTextFormat(Qt::RichText); layout->addWidget(heading); auto *version = new QLabel(tr("Version %1").arg(QApplication::applicationVersion()), &dialog); layout->addWidget(version); auto *body = new QLabel( tr("

A visual mapping tool for interactive fiction.

" "

GNU GPL v3 or later. Based on the MIT-licensed C# Trizbort by " "Genstein and Jason Lautzenheiser.

"), &dialog); body->setTextFormat(Qt::RichText); body->setWordWrap(true); layout->addWidget(body); auto *link = new QLabel( tr("https://jxself.org/trizbort-qt.shtml"), &dialog); link->setTextFormat(Qt::RichText); link->setOpenExternalLinks(true); layout->addWidget(link); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, &dialog); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); layout->addWidget(buttons); dialog.exec(); }); auto *toggleDottedAct = new QAction(tr("Toggle Dotted Lines"), this); connect(toggleDottedAct, &QAction::triggered, this, [this] { applyToSelectedConnections(tr("Toggle Dotted Lines"), [](Connection &c) { c.style = (c.style == ConnectionStyle::Dashed) ? ConnectionStyle::Solid : ConnectionStyle::Dashed; }); }); auto *toggleDirectionalAct = new QAction(tr("Toggle Directional Lines"), this); connect(toggleDirectionalAct, &QAction::triggered, this, [this] { applyToSelectedConnections(tr("Toggle Directional Lines"), [](Connection &c) { c.flow = (c.flow == ConnectionFlow::OneWay) ? ConnectionFlow::TwoWay : ConnectionFlow::OneWay; }); }); toolBar->addAction(toggleDottedAct); toolBar->addAction(toggleDirectionalAct); } bool MainWindow::deferWhileAutomapBusy(const std::function &retry) { if (!m_automapBusy) return false; abandonLiveAutomap(); QTimer::singleShot(0, this, retry); return true; } void MainWindow::newFile() { if (deferWhileAutomapBusy([this] { newFile(); })) return; if (!maybeSave()) return; abandonLiveAutomap(); m_map.clear(); // A fresh document has the default palette and a single NoRegion region. m_map.regions.append(Region{kNoRegion, QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}); m_filePath.clear(); m_scene->setDocument(&m_map); m_undo.clear(); updateTitle(); } void MainWindow::openFile() { if (deferWhileAutomapBusy([this] { openFile(); })) return; if (!maybeSave()) return; abandonLiveAutomap(); const QString path = QFileDialog::getOpenFileName( this, tr("Open Map"), QString(), tr("Trizbort maps (*.trizbort);;All files (*)")); if (path.isEmpty()) return; loadFile(path); } void MainWindow::openFromUrl() { if (deferWhileAutomapBusy([this] { openFromUrl(); })) return; if (!maybeSave()) return; abandonLiveAutomap(); bool ok = false; const QString url = QInputDialog::getText(this, tr("Open from URL"), tr("Map URL (https://…/map.trizbort):"), QLineEdit::Normal, QString(), &ok); if (!ok || url.trimmed().isEmpty()) return; QNetworkAccessManager manager; QNetworkRequest request{QUrl(url.trimmed())}; request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); request.setTransferTimeout(30000); QNetworkReply *reply = manager.get(request); QProgressDialog progress(tr("Downloading %1…").arg(url.trimmed()), tr("Cancel"), 0, 0, this); progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(0); connect(reply, &QNetworkReply::finished, &progress, &QProgressDialog::reset); connect(&progress, &QProgressDialog::canceled, reply, &QNetworkReply::abort); progress.exec(); if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::OperationCanceledError) QMessageBox::warning(this, tr("Open Failed"), tr("Could not download the map: %1").arg(reply->errorString())); reply->deleteLater(); return; } const QString xml = QString::fromUtf8(reply->readAll()); reply->deleteLater(); Map loaded; QString error; if (!TrizbortReader::loadFromString(xml, loaded, &error)) { QMessageBox::warning(this, tr("Open Failed"), tr("The download is not a valid Trizbort map: %1").arg(error)); return; } m_map = loaded; m_map.reindex(); m_filePath.clear(); // came from a URL; Save will prompt for a local path m_scene->setDocument(&m_map); m_undo.clear(); updateTitle(); m_view->zoomToFit(); statusBar()->showMessage(tr("Opened map from %1").arg(url)); } bool MainWindow::loadFile(const QString &path) { if (deferWhileAutomapBusy([this, path] { loadFile(path); })) return false; abandonLiveAutomap(); Map loaded; QString error; if (!TrizbortReader::load(path, loaded, &error)) { QMessageBox::warning(this, tr("Open Failed"), error); return false; } m_map = loaded; m_map.reindex(); m_filePath = path; m_scene->setDocument(&m_map); m_undo.clear(); updateTitle(); m_view->zoomToFit(); setWatchedFile(path); addRecentFile(path); statusBar()->showMessage(tr("Opened %1").arg(QFileInfo(path).fileName())); // Warn if the map was written by a newer format than this build produces. auto parts = [](const QString &v) { QList p; for (const QString &s : v.split(QLatin1Char('.'))) p << s.toInt(); while (p.size() < 4) p << 0; return p; }; if (!m_map.version.isEmpty()) { const QList loadedV = parts(m_map.version); const QList curV = parts(kFileFormatVersion); for (int i = 0; i < 4; ++i) { if (loadedV[i] > curV[i]) { QMessageBox::information( this, tr("Newer File Format"), tr("This map was created by a newer version of Trizbort. Some information " "may be lost if you edit and save it.")); break; } if (loadedV[i] < curV[i]) break; } } return true; } bool MainWindow::save() { if (m_filePath.isEmpty()) return saveAs(); return writeToPath(m_filePath); } bool MainWindow::saveAs() { QString path = QFileDialog::getSaveFileName(this, tr("Save Map"), m_filePath, tr("Trizbort maps (*.trizbort)")); if (path.isEmpty()) return false; if (!path.endsWith(QLatin1String(".trizbort"), Qt::CaseInsensitive)) path += QLatin1String(".trizbort"); if (!writeToPath(path)) return false; m_filePath = path; updateTitle(); return true; } bool MainWindow::writeToPath(const QString &path) { QString error; if (!TrizbortWriter::save(path, m_map, &error)) { QMessageBox::warning(this, tr("Save Failed"), error); return false; } m_undo.setClean(); m_lastSaveMs = QDateTime::currentMSecsSinceEpoch(); setWatchedFile(path); addRecentFile(path); statusBar()->showMessage(tr("Saved %1").arg(QFileInfo(path).fileName())); return true; } void MainWindow::exportMap(const QString &format) { const ExportFormat *fmt = nullptr; for (const ExportFormat &f : exportFormats()) { if (f.key == format) { fmt = &f; break; } } if (!fmt) return; const QString suggested = m_filePath.isEmpty() ? fmt->key : QFileInfo(m_filePath).completeBaseName(); QString filter = tr("%1 (*.%2)").arg(fmt->label, fmt->extension); if (format == QLatin1String("inform7")) filter += tr(";;Inform 7 project (*.inform)"); filter += tr(";;All files (*)"); QString path = QFileDialog::getSaveFileName( this, tr("Export as %1").arg(fmt->label), suggested + QLatin1Char('.') + fmt->extension, filter); if (path.isEmpty()) return; auto exporter = makeExporter(format, m_map, m_filePath); if (!exporter) return; const QString text = exporter->exportToString(); QString error; if (!writeExportText(path, text, &error)) { QMessageBox::warning(this, tr("Export Failed"), error); return; } statusBar()->showMessage(tr("Exported %1").arg(QFileInfo(exportOutputPath(path)).fileName())); } void MainWindow::exportImage() { const QString suggested = (m_filePath.isEmpty() ? QStringLiteral("map") : QFileInfo(m_filePath).completeBaseName()) + QStringLiteral(".png"); QString path = QFileDialog::getSaveFileName( this, tr("Export Image"), suggested, tr("PNG image (*.png);;JPEG image (*.jpg *.jpeg);;Bitmap (*.bmp);;All files (*)")); if (path.isEmpty()) return; // Default to PNG if the user gave no recognized image extension. const QString suffix = QFileInfo(path).suffix().toLower(); if (suffix != QLatin1String("png") && suffix != QLatin1String("jpg") && suffix != QLatin1String("jpeg") && suffix != QLatin1String("bmp")) path += QStringLiteral(".png"); QString error; const double scale = m_saveAt100 ? 1.0 : m_view->transform().m11(); if (!renderMapToImage(m_map, path, &error, scale)) QMessageBox::warning(this, tr("Export Failed"), error); else statusBar()->showMessage(tr("Exported %1").arg(QFileInfo(path).fileName())); } void MainWindow::exportPdf() { const QString suggested = (m_filePath.isEmpty() ? QStringLiteral("map") : QFileInfo(m_filePath).completeBaseName()) + QStringLiteral(".pdf"); QString path = QFileDialog::getSaveFileName(this, tr("Export PDF"), suggested, tr("PDF document (*.pdf);;All files (*)")); if (path.isEmpty()) return; QString error; if (!renderMapToPdf(m_map, path, &error)) QMessageBox::warning(this, tr("Export Failed"), error); else statusBar()->showMessage(tr("Exported %1").arg(QFileInfo(path).fileName())); } void MainWindow::importTranscript() { const QString path = QFileDialog::getOpenFileName( this, tr("Import Transcript"), QString(), tr("Transcripts (*.txt *.log *.scr);;All files (*)")); if (path.isEmpty()) return; QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Import Failed"), tr("Could not read %1").arg(path)); return; } const QString text = QString::fromUtf8(file.readAll()); ReplaceContentCommand::Content before{m_map.rooms, m_map.connections, m_map.regions}; Map working = m_map; AutomapSettings settings; settings.preferredDistanceBetweenRooms = m_map.settings.preferredDistanceBetweenRooms; settings.gridSize = m_map.settings.gridSize; working.reindex(); GuiAutomapController controller(this); controller.setLabelFn([&working](int id) -> QString { const Room *r = working.roomById(id); if (!r) return tr("Room %1").arg(id); return QStringLiteral("%1 (%2, %3)").arg(r->name).arg(qRound(r->x)).arg(qRound(r->y)); }); QString normalized = text; normalized.replace(QLatin1String("\r\n"), QLatin1String("\n")); normalized.replace(QLatin1Char('\r'), QLatin1Char('\n')); TranscriptAutomapper mapper(settings); mapper.begin(working, &controller); mapper.feed(normalized.split(QLatin1Char('\n')), true); const int added = mapper.roomsAdded(); if (added == 0) { QMessageBox::information(this, tr("Import Transcript"), tr("No rooms were found in the transcript.")); return; } ReplaceContentCommand::Content after{working.rooms, working.connections, working.regions}; m_undo.push(new ReplaceContentCommand(m_scene, before, after, tr("Import Transcript"))); m_view->zoomToFit(); statusBar()->showMessage(tr("Imported %1: added %2 rooms, %3 connections") .arg(QFileInfo(path).fileName()) .arg(added) .arg(mapper.connectionsAdded())); } void MainWindow::startLiveAutomap() { if (m_automapping) { statusBar()->showMessage(tr("Automapping is already running.")); return; } // Options dialog (transcript, toggles, command words, step mode), seeded // from the persisted automap preferences. QSettings s; AutomapSettings settings; settings.assumeTwoWayConnections = s.value(QStringLiteral("automap/twoWay"), settings.assumeTwoWayConnections).toBool(); settings.assumeSameNameSameRoom = s.value(QStringLiteral("automap/sameName"), settings.assumeSameNameSameRoom).toBool(); settings.verboseTranscript = s.value(QStringLiteral("automap/verbose"), settings.verboseTranscript).toBool(); settings.continueTranscript = s.value(QStringLiteral("automap/continue"), settings.continueTranscript).toBool(); settings.guessExits = s.value(QStringLiteral("automap/guessExits"), settings.guessExits).toBool(); settings.objectCommand = s.value(QStringLiteral("automap/objectCommand"), QStringLiteral("tb see")).toString(); settings.regionCommand = s.value(QStringLiteral("automap/regionCommand"), QStringLiteral("tb region")).toString(); settings.preferredDistanceBetweenRooms = m_map.settings.preferredDistanceBetweenRooms; settings.gridSize = m_map.settings.gridSize; AutomapOptionsDialog dialog(settings, this); if (dialog.exec() != QDialog::Accepted) return; const QString path = dialog.transcriptPath(); if (path.isEmpty()) { statusBar()->showMessage(tr("No transcript chosen.")); return; } settings = dialog.settings(); s.setValue(QStringLiteral("automap/twoWay"), settings.assumeTwoWayConnections); s.setValue(QStringLiteral("automap/sameName"), settings.assumeSameNameSameRoom); s.setValue(QStringLiteral("automap/verbose"), settings.verboseTranscript); s.setValue(QStringLiteral("automap/continue"), settings.continueTranscript); s.setValue(QStringLiteral("automap/guessExits"), settings.guessExits); s.setValue(QStringLiteral("automap/objectCommand"), settings.objectCommand); s.setValue(QStringLiteral("automap/regionCommand"), settings.regionCommand); // Snapshot for one-step undo. m_automapPreMap = m_map; m_automapPath = path; m_automapOffset = 0; m_automapPartialLine.clear(); m_automapStopRequested = false; m_automapAbandonRequested = false; // "Start from the end": treat whatever is already in the transcript as // already seen, so only moves played after this point are mapped (the C# // ContinueTranscript option). if (settings.continueTranscript) { QFile existing(path); if (existing.open(QIODevice::ReadOnly)) m_automapOffset = existing.size(); } m_automapping = true; m_automapController = new GuiAutomapController(this); m_automapController->setStepping(dialog.stepping()); m_automapController->setStatusFn( [this](const QString &msg) { statusBar()->showMessage(msg); }); m_automapController->setLabelFn([this](int id) -> QString { const Room *r = m_map.roomById(id); if (!r) return tr("Room %1").arg(id); return QStringLiteral("%1 (%2, %3)").arg(r->name).arg(qRound(r->x)).arg(qRound(r->y)); }); m_automapper = new TranscriptAutomapper(settings); m_automapper->begin(m_map, m_automapController); if (m_automapStopButton) m_automapStopButton->setVisible(true); if (m_automapStopAction) m_automapStopAction->setEnabled(true); if (m_automapStepAction) m_automapStepAction->setEnabled(dialog.stepping()); if (m_automapRunAction) m_automapRunAction->setEnabled(dialog.stepping()); if (!m_automapTimer) { m_automapTimer = new QTimer(this); m_automapTimer->setInterval(400); connect(m_automapTimer, &QTimer::timeout, this, &MainWindow::automapTick); } m_automapTimer->start(); statusBar()->showMessage( tr("Live automap: following %1 — play your game, then Stop.").arg(QFileInfo(path).fileName())); automapTick(); // process whatever is already there } void MainWindow::automapTick() { if (!m_automapping || m_automapBusy || !m_automapper) return; QFile file(m_automapPath); if (!file.open(QIODevice::ReadOnly)) return; const qint64 size = file.size(); if (size < m_automapOffset) { m_automapOffset = 0; m_automapPartialLine.clear(); } if (size == m_automapOffset) return; if (!file.seek(m_automapOffset)) return; QByteArray appended = file.readAll(); m_automapOffset += appended.size(); appended.prepend(m_automapPartialLine); m_automapPartialLine.clear(); const qsizetype lastNewline = appended.lastIndexOf('\n'); if (lastNewline < 0) { m_automapPartialLine = appended; return; } m_automapPartialLine = appended.mid(lastNewline + 1); QString text = QString::fromUtf8(appended.left(lastNewline)); text.remove(QLatin1Char('\r')); const QStringList newLines = text.split(QLatin1Char('\n')); // Feed only the new lines to the incremental engine (final=false: the last // room waits for its next command or for Stop). step() may block on a nested // event loop while single-stepping, so guard against re-entrant ticks. m_automapBusy = true; const bool ok = m_automapper->feed(newLines, /*final=*/false); m_automapBusy = false; m_map.reindex(); m_scene->setDocument(&m_map); setWindowModified(true); if (m_automapAbandonRequested) { abandonLiveAutomap(); return; } if (!ok || m_automapStopRequested) { stopLiveAutomap(); return; } statusBar()->showMessage(tr("Live automap: %1 rooms, %2 connections so far…") .arg(m_map.rooms.size()) .arg(m_map.connections.size())); } void MainWindow::abandonLiveAutomap() { if (!m_automapping) return; if (m_automapBusy) { m_automapAbandonRequested = true; if (m_automapController) m_automapController->cancel(); return; } if (m_automapTimer) m_automapTimer->stop(); if (m_automapController) m_automapController->cancel(); m_automapping = false; m_automapAbandonRequested = false; m_automapStopRequested = false; delete m_automapper; m_automapper = nullptr; delete m_automapController; m_automapController = nullptr; if (m_automapStopButton) m_automapStopButton->setVisible(false); if (m_automapStopAction) m_automapStopAction->setEnabled(false); if (m_automapStepAction) m_automapStepAction->setEnabled(false); if (m_automapRunAction) m_automapRunAction->setEnabled(false); } void MainWindow::stopLiveAutomap() { if (!m_automapping) return; if (m_automapBusy) { m_automapStopRequested = true; if (m_automapController) m_automapController->runToCompletion(); return; } if (m_automapTimer) m_automapTimer->stop(); m_automapping = false; m_automapStopRequested = false; if (m_automapStopButton) m_automapStopButton->setVisible(false); if (m_automapStopAction) m_automapStopAction->setEnabled(false); if (m_automapStepAction) m_automapStepAction->setEnabled(false); if (m_automapRunAction) m_automapRunAction->setEnabled(false); // Flush the final room block, then tear down the engine/controller. if (m_automapper) { m_automapController->runToCompletion(); // release any pending step gate QStringList tail; if (!m_automapPartialLine.trimmed().isEmpty()) { QString last = QString::fromUtf8(m_automapPartialLine); last.remove(QLatin1Char('\r')); tail << last; } m_automapPartialLine.clear(); m_automapper->feed(tail, /*final=*/true); m_map.reindex(); m_scene->setDocument(&m_map); delete m_automapper; m_automapper = nullptr; } delete m_automapController; m_automapController = nullptr; // Record the whole automap session as a single undoable step. ReplaceContentCommand::Content before{m_automapPreMap.rooms, m_automapPreMap.connections, m_automapPreMap.regions}; ReplaceContentCommand::Content after{m_map.rooms, m_map.connections, m_map.regions}; const bool changed = before.rooms != after.rooms || before.connections != after.connections || before.regions.size() != after.regions.size(); if (changed) m_undo.push(new ReplaceContentCommand(m_scene, before, after, tr("Live Automap"))); else setWindowModified(!m_undo.isClean()); m_view->zoomToFit(); statusBar()->showMessage(tr("Stopped automapping: %1 rooms, %2 connections.") .arg(m_map.rooms.size()) .arg(m_map.connections.size())); } void MainWindow::addRoom() { const QPointF center = m_view->mapToScene(m_view->viewport()->rect().center()); const int id = m_scene->addRoomAt(center); if (id >= 0) statusBar()->showMessage(tr("Added room %1 (double-click to edit)").arg(id)); } void MainWindow::addConnectedRoom(const QString &direction) { const int fromId = m_scene->selectedRoomId(); if (fromId < 0) { statusBar()->showMessage(tr("Select a room first, then add a connected room.")); return; } auto *cmd = new AddConnectedRoomCommand(m_scene, fromId, direction); if (!cmd->valid()) { delete cmd; return; } const int newId = cmd->newRoomId(); m_undo.push(cmd); statusBar()->showMessage(tr("Added room %1 (%2)").arg(newId).arg(direction.toUpper())); } void MainWindow::addConnectedRoomLabeled(const QString &placementDir, const QString &startLabel, const QString &endLabel, const QString &displayName) { const int fromId = m_scene->selectedRoomId(); if (fromId < 0) { statusBar()->showMessage(tr("Select a room first, then add a connected room.")); return; } auto *cmd = new AddConnectedRoomCommand(m_scene, fromId, placementDir, startLabel, endLabel); if (!cmd->valid()) { delete cmd; return; } const int newId = cmd->newRoomId(); m_undo.push(cmd); statusBar()->showMessage(tr("Added room %1 (%2)").arg(newId).arg(displayName)); } namespace { // The eight compass directions in clockwise order, for ±45° rotation. const char *const kCompassRing[8] = {"n", "ne", "e", "se", "s", "sw", "w", "nw"}; int compassIndex(const QString &dir) { for (int i = 0; i < 8; ++i) if (dir == QLatin1String(kCompassRing[i])) return i; return -1; } QString rotateCompass(const QString &dir, int steps) { const int i = compassIndex(dir); if (i < 0) return dir; return QLatin1String(kCompassRing[((i + steps) % 8 + 8) % 8]); } // A unit step in map coordinates for a compass token (y grows downward). QPointF dirStep(const QString &dir) { double dx = 0, dy = 0; if (dir.contains(QLatin1Char('n'))) dy = -1; if (dir.contains(QLatin1Char('s'))) dy = 1; if (dir.contains(QLatin1Char('e'))) dx = 1; if (dir.contains(QLatin1Char('w'))) dx = -1; return QPointF(dx, dy); } // Compass token for a key press: arrows give cardinals; keypad keys add the // diagonals (and cover both NumLock states, e.g. Key_8 or Key_Up == north). QString compassFromKey(int key, bool keypad) { switch (key) { case Qt::Key_Up: return QStringLiteral("n"); case Qt::Key_Down: return QStringLiteral("s"); case Qt::Key_Left: return QStringLiteral("w"); case Qt::Key_Right: return QStringLiteral("e"); default: break; } if (keypad) { switch (key) { case Qt::Key_8: return QStringLiteral("n"); case Qt::Key_2: return QStringLiteral("s"); case Qt::Key_4: return QStringLiteral("w"); case Qt::Key_6: return QStringLiteral("e"); case Qt::Key_7: case Qt::Key_Home: return QStringLiteral("nw"); case Qt::Key_9: case Qt::Key_PageUp: return QStringLiteral("ne"); case Qt::Key_1: case Qt::Key_End: return QStringLiteral("sw"); case Qt::Key_3: case Qt::Key_PageDown: return QStringLiteral("se"); default: break; } } return QString(); } Qt::KeyboardModifier parseKeypadModifier(const QString &name) { const QString n = name.toLower(); if (n == QLatin1String("control")) return Qt::ControlModifier; if (n == QLatin1String("alt")) return Qt::AltModifier; if (n == QLatin1String("shift")) return Qt::ShiftModifier; return Qt::NoModifier; } } // namespace bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == m_view && event->type() == QEvent::KeyPress) { if (handleCanvasKey(static_cast(event))) return true; } return QMainWindow::eventFilter(obj, event); } bool MainWindow::handleCanvasKey(QKeyEvent *event) { const int key = event->key(); const Qt::KeyboardModifiers m = event->modifiers(); if (key == Qt::Key_Return || key == Qt::Key_Enter) { const int rid = m_scene->selectedRoomId(); if (rid >= 0) { editRoom(rid); return true; } const QList cids = m_scene->selectedConnectionIds(); if (!cids.isEmpty()) { editConnection(cids.first()); return true; } const QPointF center = m_view->mapToScene(m_view->viewport()->rect().center()); int best = -1; double bestD2 = 1e30; for (const Room &r : m_map.rooms) { const double dx = r.x + r.w / 2.0 - center.x(); const double dy = r.y + r.h / 2.0 - center.y(); const double d2 = dx * dx + dy * dy; if (d2 < bestD2) { bestD2 = d2; best = r.id; } } if (best >= 0) m_scene->selectRoomItem(best); return true; } if (key == Qt::Key_0 && !(m & (Qt::ControlModifier | Qt::AltModifier))) { for (const Room &r : m_map.rooms) { if (r.isStartRoom) { m_scene->selectRoomItem(r.id); m_view->ensureVisible(QRectF(r.x, r.y, r.w, r.h)); break; } } return true; } const bool keypad = event->modifiers() & Qt::KeypadModifier; const QString dir = compassFromKey(event->key(), keypad); if (dir.isEmpty()) return false; const Qt::KeyboardModifiers mods = event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier); const bool ctrl = mods & Qt::ControlModifier; const bool alt = mods & Qt::AltModifier; const bool shift = mods & Qt::ShiftModifier; auto followTo = [this](const QString &d) { const int r = roomThroughConnection(m_scene->selectedRoomId(), d); if (r >= 0) { m_scene->selectRoomItem(r); if (const Room *rr = m_map.roomById(r)) m_view->ensureVisible(QRectF(rr->x, rr->y, rr->w, rr->h)); } }; if (keypad) { // Numeric-keypad navigation (diagonals included). Shift follows a // connection; otherwise navigate, creating/stubbing per the map's // keypad modifiers. if (shift) { followTo(dir); return true; } const Qt::KeyboardModifier createMod = parseKeypadModifier(m_map.settings.keypadCreationModifier); const Qt::KeyboardModifier unexpMod = parseKeypadModifier(m_map.settings.keypadUnexploredModifier); const bool create = createMod != Qt::NoModifier && (mods & createMod); const bool unexplored = unexpMod != Qt::NoModifier && (mods & unexpMod); navigateOrAdd(dir, create, unexplored); return true; } // Plain arrow keys (cardinals only). if (ctrl && alt) { keyboardResizeRooms(dir); return true; } if (ctrl) { navigateOrAdd(dir, /*allowCreate=*/true, /*unexploredStub=*/false); return true; } if (shift) { followTo(dir); return true; } // No modifier: nudge the selection, or scroll the view when nothing is // selected. if (m_scene->selectedRoomIds().isEmpty()) { const QPointF s = dirStep(dir); auto *hb = m_view->horizontalScrollBar(); auto *vb = m_view->verticalScrollBar(); hb->setValue(hb->value() + int(s.x()) * (m_view->viewport()->width() / 10)); vb->setValue(vb->value() + int(s.y()) * (m_view->viewport()->height() / 10)); return true; } const double delta = m_map.settings.snapToGrid ? m_map.settings.gridSize : 2.0; const QPointF s = dirStep(dir); nudgeSelection(s.x() * delta, s.y() * delta); return true; } int MainWindow::roomThroughConnection(int roomId, const QString &dir) const { if (roomId < 0) return -1; // Try the exact direction, then 45° either side (the C# approximate match). const QString tries[3] = {dir, rotateCompass(dir, -1), rotateCompass(dir, 1)}; for (const QString &d : tries) { for (const Connection &c : m_map.connections) { bool fromHere = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId && v.port.toLower() == d) { fromHere = true; break; } } if (!fromHere) continue; for (const Vertex &v : c.vertices) if (v.docked && v.roomId != roomId) return v.roomId; } } return -1; } void MainWindow::nudgeSelection(double dx, double dy) { const QList rooms = m_scene->selectedRoomIds(); QList moves; for (int id : rooms) { const Room *r = m_map.roomById(id); if (!r) continue; moves.append({id, QPointF(r->x, r->y), QPointF(r->x + dx, r->y + dy)}); } if (!moves.isEmpty()) m_undo.push(new MoveRoomsCommand(m_scene, moves)); } void MainWindow::keyboardResizeRooms(const QString &dir) { const double delta = m_map.settings.snapToGrid ? m_map.settings.gridSize : 2.0; const double grid = m_map.settings.gridSize; applyToSelectedRooms(tr("Resize Room"), [dir, delta, grid](Room &r) { if (dir == QLatin1String("w")) { if (r.w - delta >= grid) r.w -= delta; } else if (dir == QLatin1String("e")) { r.w += delta; } else if (dir == QLatin1String("n")) { if (r.h - delta >= grid) r.h -= delta; } else if (dir == QLatin1String("s")) { r.h += delta; } }); } void MainWindow::navigateOrAdd(const QString &dir, bool allowCreate, bool unexploredStub) { const int sel = m_scene->selectedRoomId(); if (sel < 0) { statusBar()->showMessage(tr("Select a room first.")); return; } const int target = roomThroughConnection(sel, dir); if (target >= 0) { m_scene->selectRoomItem(target); if (const Room *rr = m_map.roomById(target)) m_view->ensureVisible(QRectF(rr->x, rr->y, rr->w, rr->h)); return; } if (allowCreate) addConnectedRoom(dir); else if (unexploredStub) addUnexploredExit(sel, dir); } void MainWindow::addUnexploredExit(int roomId, const QString &dir) { const Room *r = m_map.roomById(roomId); if (!r) return; Connection c; c.id = m_map.nextConnectionId(); c.seq = m_map.nextSeq(); c.style = m_scene->newConnectionStyle(); c.flow = m_scene->newConnectionFlow(); Vertex a; a.index = 0; a.docked = true; a.roomId = roomId; a.port = dir; const QPointF start = MapScene::portPoint(*r, dir); const QPointF step = dirStep(dir); const double dist = m_map.settings.preferredDistanceBetweenRooms; Vertex b; b.index = 1; b.docked = false; b.point = QPointF(start.x() + step.x() * dist, start.y() + step.y() * dist); c.vertices << a << b; ReplaceContentCommand::Content before{m_map.rooms, m_map.connections, m_map.regions}; ReplaceContentCommand::Content after = before; after.connections.append(c); m_undo.push(new ReplaceContentCommand(m_scene, before, after, tr("Add Unexplored Exit"))); } void MainWindow::deleteSelection() { m_scene->deleteSelection(); } void MainWindow::toggleConnectMode(bool on) { m_scene->setConnectMode(on); statusBar()->showMessage(on ? tr("Connect mode: drag between two rooms to connect them.") : tr("Select mode.")); } void MainWindow::editRoom(int roomId) { const Room *room = m_map.roomById(roomId); if (!room) return; const Room before = *room; RoomDialog dialog(m_map, before, this); if (dialog.exec() != QDialog::Accepted) return; const Room after = dialog.result(); if (after == before || !m_map.roomById(roomId)) return; m_undo.push(new EditRoomCommand(m_scene, roomId, before, after)); } void MainWindow::editConnection(int connId) { const int idx = m_map.connectionIndex(connId); if (idx < 0) return; const Connection before = m_map.connections.at(idx); ConnectionDialog dialog(before, this); if (dialog.exec() != QDialog::Accepted) return; const Connection after = dialog.result(); if (after == before || m_map.connectionIndex(connId) < 0) return; m_undo.push(new EditConnectionCommand(m_scene, connId, before, after)); } void MainWindow::applyToSelectedRooms(const QString &label, const std::function &fn) { const QList ids = m_scene->selectedRoomIds(); if (ids.isEmpty()) { statusBar()->showMessage(tr("Select one or more rooms first.")); return; } m_undo.beginMacro(label); for (int id : ids) { const Room *r = m_map.roomById(id); if (!r) continue; Room before = *r; Room after = before; fn(after); m_undo.push(new EditRoomCommand(m_scene, id, before, after)); } m_undo.endMacro(); } void MainWindow::applyToSelectedConnections(const QString &label, const std::function &fn) { const QList ids = m_scene->selectedConnectionIds(); if (ids.isEmpty()) { statusBar()->showMessage(tr("Select one or more connections first.")); return; } m_undo.beginMacro(label); for (int id : ids) { const int idx = m_map.connectionIndex(id); if (idx < 0) continue; const Connection before = m_map.connections.at(idx); Connection after = before; fn(after); m_undo.push(new EditConnectionCommand(m_scene, id, before, after)); } m_undo.endMacro(); } void MainWindow::reverseSelectedConnections() { applyToSelectedConnections(tr("Reverse Connection"), [](Connection &c) { std::reverse(c.vertices.begin(), c.vertices.end()); for (int i = 0; i < c.vertices.size(); ++i) c.vertices[i].index = i; std::swap(c.startText, c.endText); }); } bool MainWindow::orthogonalizeConnection(Connection &c) const { if (c.vertices.size() != 2) return false; std::sort(c.vertices.begin(), c.vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); const Vertex v0 = c.vertices.first(); const Vertex v1 = c.vertices.last(); if (!v0.docked || !v1.docked) return false; const Room *r0 = m_map.roomById(v0.roomId); const Room *r1 = m_map.roomById(v1.roomId); if (!r0 || !r1) return false; auto axisOf = [](const QString &p) -> int { const QString t = p.toLower(); if (t == QLatin1String("n") || t == QLatin1String("s")) return 1; if (t == QLatin1String("e") || t == QLatin1String("w")) return 2; return 0; }; const int a0 = axisOf(v0.port); const int a1 = axisOf(v1.port); if (a0 == 0 || a1 == 0) return false; const double stalk = m_map.settings.connectionStalkLength; const QPointF s0 = MapScene::portStalkPoint(*r0, v0.port, stalk); const QPointF s1 = MapScene::portStalkPoint(*r1, v1.port, stalk); const double eps = 0.5; if (std::abs(s0.x() - s1.x()) < eps || std::abs(s0.y() - s1.y()) < eps) return false; QVector mids; if (a0 == 1 && a1 == 2) mids.append(QPointF(s0.x(), s1.y())); else if (a0 == 2 && a1 == 1) mids.append(QPointF(s1.x(), s0.y())); else if (a0 == 1 && a1 == 1) { const double my = (s0.y() + s1.y()) / 2.0; mids.append(QPointF(s0.x(), my)); mids.append(QPointF(s1.x(), my)); } else { const double mx = (s0.x() + s1.x()) / 2.0; mids.append(QPointF(mx, s0.y())); mids.append(QPointF(mx, s1.y())); } QList rebuilt; rebuilt.append(v0); for (const QPointF &p : mids) { Vertex w; w.docked = false; w.point = p; rebuilt.append(w); } rebuilt.append(v1); for (int i = 0; i < rebuilt.size(); ++i) rebuilt[i].index = i; c.vertices = rebuilt; return true; } void MainWindow::makeSelectedConnectionsOrthogonal() { const QList ids = m_scene->selectedConnectionIds(); if (ids.isEmpty()) { statusBar()->showMessage(tr("Select one or more connections first.")); return; } struct Pending { int id; Connection before; Connection after; }; QVector pending; for (int id : ids) { const int idx = m_map.connectionIndex(id); if (idx < 0) continue; const Connection before = m_map.connections.at(idx); Connection after = before; if (orthogonalizeConnection(after)) pending.append({id, before, after}); } if (pending.isEmpty()) { statusBar()->showMessage( tr("Nothing to do: needs a connection with two cardinal-port endpoints.")); return; } m_undo.beginMacro(tr("Make Orthogonal")); for (const Pending &p : pending) m_undo.push(new EditConnectionCommand(m_scene, p.id, p.before, p.after)); m_undo.endMacro(); } void MainWindow::insertRoomOnConnection() { const QList sel = m_scene->selectedConnectionIds(); if (sel.size() != 1) { statusBar()->showMessage(tr("Select exactly one connection to split.")); return; } const int cid = sel.first(); const int idx = m_map.connectionIndex(cid); if (idx < 0) return; Connection orig = m_map.connections.at(idx); std::sort(orig.vertices.begin(), orig.vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); if (orig.vertices.size() < 2) return; const Vertex first = orig.vertices.first(); const Vertex last = orig.vertices.last(); // Midpoint (scene) for the new room. auto vertexPoint = [this](const Vertex &v) -> QPointF { if (v.docked) { if (const Room *r = m_map.roomById(v.roomId)) return MapScene::portPoint(*r, v.port); } return v.point; }; const QPointF mid = (vertexPoint(first) + vertexPoint(last)) / 2.0; ReplaceContentCommand::Content before{m_map.rooms, m_map.connections, m_map.regions}; ReplaceContentCommand::Content after = before; int nextId = m_map.nextConnectionId(); int maxSeq = m_map.nextSeq() - 1; Room newRoom; newRoom.id = nextId++; newRoom.seq = ++maxSeq; newRoom.name = m_map.settings.defaultRoomName; newRoom.w = 96.0; newRoom.h = 64.0; const QPointF tl = m_scene->snap(QPointF(mid.x() - newRoom.w / 2.0, mid.y() - newRoom.h / 2.0)); newRoom.x = tl.x(); newRoom.y = tl.y(); // Inherit the region when both ends are in the same one. if (first.docked && last.docked) { const Room *ra = m_map.roomById(first.roomId); const Room *rb = m_map.roomById(last.roomId); if (ra && rb && ra->region == rb->region) newRoom.region = ra->region; } after.rooms.append(newRoom); // Compute the ports the new room presents to each neighbor from geometry: // the port facing the source takes the incoming segment, the port facing the // target takes the outgoing one (the C# crossover of source/target compass // points, generalized to any orientation and to dangling ends). const QPointF sourcePoint = vertexPoint(first); const QPointF targetPoint = vertexPoint(last); const QString incomingPort = MapScene::portTowards(newRoom, sourcePoint); const QString outgoingPort = MapScene::portTowards(newRoom, targetPoint); // Re-target the original connection's far end onto the new room, and add a // second connection from the new room to the original target. for (Connection &c : after.connections) { if (c.id != cid) continue; std::sort(c.vertices.begin(), c.vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); Vertex &end = c.vertices.last(); end.docked = true; end.roomId = newRoom.id; end.port = incomingPort; break; } Connection second; second.id = nextId++; second.seq = ++maxSeq; second.style = orig.style; second.flow = orig.flow; Vertex a; a.index = 0; a.docked = true; a.roomId = newRoom.id; a.port = outgoingPort; Vertex b = last; // original far endpoint b.index = 1; second.vertices << a << b; after.connections.append(second); m_undo.push(new ReplaceContentCommand(m_scene, before, after, tr("Insert Room on Connection"))); m_scene->selectRoomItem(newRoom.id); statusBar()->showMessage(tr("Inserted a room on the connection.")); } void MainWindow::rotateSelectedConnectors(bool source) { static const char *const kPorts[] = {"n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"}; applyToSelectedConnections(source ? tr("Rotate Source Port") : tr("Rotate Target Port"), [source](Connection &c) { if (c.vertices.isEmpty()) return; std::sort(c.vertices.begin(), c.vertices.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); for (int i = 0; i < c.vertices.size(); ++i) c.vertices[i].index = i; Vertex &v = source ? c.vertices.first() : c.vertices.last(); if (!v.docked) return; int cur = 0; for (int k = 0; k < 16; ++k) if (v.port.toLower() == QLatin1String(kPorts[k])) { cur = k; break; } v.port = QString::fromLatin1(kPorts[(cur + 1) % 16]); }); } void MainWindow::setStartOrEndRoom(bool start) { const QList sel = m_scene->selectedRoomIds(); if (sel.isEmpty()) { statusBar()->showMessage(tr("Select a room first.")); return; } const int target = sel.first(); // start/end room is unique QList changed; for (const Room &room : m_map.rooms) { const bool cur = start ? room.isStartRoom : room.isEndRoom; if (cur != (room.id == target)) changed.append(room.id); } if (changed.isEmpty()) return; m_undo.beginMacro(start ? tr("Set Start Room") : tr("Set End Room")); for (int id : changed) { const Room *r = m_map.roomById(id); if (!r) continue; Room before = *r; Room after = before; (start ? after.isStartRoom : after.isEndRoom) = (id == target); m_undo.push(new EditRoomCommand(m_scene, id, before, after)); } m_undo.endMacro(); } void MainWindow::setSelectedRoomShape(RoomShape shape) { applyToSelectedRooms(tr("Set Room Shape"), [shape](Room &r) { r.ellipse = r.octagonal = r.roundedCorners = false; if (shape == RoomShape::RoundedCorners) r.roundedCorners = true; else if (shape == RoomShape::Ellipse) r.ellipse = true; else if (shape == RoomShape::Octagonal) r.octagonal = true; }); } void MainWindow::setSelectedRoomsZOrder(bool toFront) { const int z = toFront ? m_map.highestZOrder() : m_map.lowestZOrder(); applyToSelectedRooms(toFront ? tr("Bring to Front") : tr("Send to Back"), [z](Room &r) { r.zOrder = z; }); } void MainWindow::deleteConnectionsOfSelectedRoom() { const QList sel = m_scene->selectedRoomIds(); if (sel.size() != 1) { statusBar()->showMessage(tr("Select exactly one room first.")); return; } const int roomId = sel.first(); QList connIds; for (const Connection &c : m_map.connections) { for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId) { connIds.append(c.id); break; } } } if (connIds.isEmpty()) { statusBar()->showMessage(tr("No connections were deleted.")); return; } auto *cmd = new DeleteElementsCommand(m_scene, QList(), connIds); cmd->setText(tr("Delete Room's Connections")); m_undo.push(cmd); m_scene->selectRoomItem(roomId); statusBar()->showMessage(tr("Deleted %1 connections.").arg(connIds.size())); } void MainWindow::selectRoomsInSelectedRegions() { const QList sel = m_scene->selectedRoomIds(); if (sel.isEmpty()) { statusBar()->showMessage(tr("Select one or more rooms first.")); return; } QSet regions; for (int id : sel) { if (const Room *r = m_map.roomById(id)) regions.insert(normalizeRegionName(r->region)); } QList ids; for (const Room &r : m_map.rooms) { if (regions.contains(normalizeRegionName(r.region))) ids.append(r.id); } m_scene->selectRoomsByIds(ids); statusBar()->showMessage(tr("Selected %1 rooms.").arg(ids.size())); } void MainWindow::showCanvasContextMenu(const QPointF &scenePos, const QPoint &screenPos) { QMenu menu(this); menu.addAction(tr("Add &Room Here"), this, [this, scenePos] { const int id = m_scene->addRoomAt(scenePos); if (id >= 0) statusBar()->showMessage(tr("Added room %1 (double-click to edit)").arg(id)); }); menu.addAction(tr("&Paste"), this, &MainWindow::paste); menu.addSeparator(); menu.addAction(tr("Select &All"), this, [this] { m_scene->selectAll(); }); menu.addSeparator(); menu.addAction(tr("&Map Settings…"), this, &MainWindow::editMapSettings); menu.addAction(tr("A&pplication Settings…"), this, &MainWindow::showAppSettings); menu.exec(screenPos); } void MainWindow::joinSelectedRooms() { const QList sel = m_scene->selectedRoomIds(); if (sel.size() != 2) { statusBar()->showMessage(tr("Select exactly two rooms to join.")); return; } const Room *a = m_map.roomById(sel.at(0)); const Room *b = m_map.roomById(sel.at(1)); if (!a || !b) return; // Skip if a connection already docks both of these rooms. for (const Connection &c : m_map.connections) { bool touchesA = false; bool touchesB = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == a->id) touchesA = true; if (v.docked && v.roomId == b->id) touchesB = true; } if (touchesA && touchesB) { statusBar()->showMessage(tr("Those rooms are already connected.")); return; } } m_undo.push(new AddConnectionCommand(m_scene, a->id, Map::portFacing(*a, *b), b->id, Map::portFacing(*b, *a))); } void MainWindow::swapSelectedRooms(SwapMode mode) { const QList sel = m_scene->selectedRoomIds(); if (sel.size() != 2) { statusBar()->showMessage(tr("Select exactly two rooms to swap.")); return; } const Room *ra = m_map.roomById(sel.at(0)); const Room *rb = m_map.roomById(sel.at(1)); if (!ra || !rb) return; const Room a = *ra; const Room b = *rb; Room na = a; Room nb = b; auto copyFormat = [](Room &dst, const Room &src) { dst.fill = src.fill; dst.secondFill = src.secondFill; dst.secondFillLocation = src.secondFillLocation; dst.border = src.border; dst.largeText = src.largeText; dst.subtitleColor = src.subtitleColor; dst.smallText = src.smallText; dst.borderStyle = src.borderStyle; dst.ellipse = src.ellipse; dst.octagonal = src.octagonal; dst.roundedCorners = src.roundedCorners; dst.allCornersEqual = src.allCornersEqual; dst.cornerTopLeft = src.cornerTopLeft; dst.cornerTopRight = src.cornerTopRight; dst.cornerBottomLeft = src.cornerBottomLeft; dst.cornerBottomRight = src.cornerBottomRight; dst.isDark = src.isDark; dst.handDrawn = src.handDrawn; }; switch (mode) { case SwapMode::Objects: na.objectsText = b.objectsText; na.objectsPosition = b.objectsPosition; nb.objectsText = a.objectsText; nb.objectsPosition = a.objectsPosition; break; case SwapMode::Names: na.name = b.name; na.subtitle = b.subtitle; nb.name = a.name; nb.subtitle = a.subtitle; break; case SwapMode::Formats: copyFormat(na, b); copyFormat(nb, a); break; case SwapMode::Regions: na.region = b.region; nb.region = a.region; break; } m_undo.beginMacro(tr("Swap Rooms")); m_undo.push(new EditRoomCommand(m_scene, a.id, a, na)); m_undo.push(new EditRoomCommand(m_scene, b.id, b, nb)); m_undo.endMacro(); } void MainWindow::renameSelectedRoom() { const int id = m_scene->selectedRoomId(); const Room *r = m_map.roomById(id); if (!r) { statusBar()->showMessage(tr("Select a room to rename.")); return; } bool ok = false; const QString name = QInputDialog::getText(this, tr("Rename Room"), tr("Name:"), QLineEdit::Normal, r->name, &ok); if (!ok) return; Room before = *r; Room after = before; after.name = name; m_undo.push(new EditRoomCommand(m_scene, id, before, after)); } void MainWindow::changeSelectedRegion() { if (m_scene->selectedRoomIds().isEmpty()) { statusBar()->showMessage(tr("Select one or more rooms first.")); return; } QStringList regions; for (const Region &rg : m_map.regions) regions << rg.name; if (regions.isEmpty()) regions << kNoRegion; bool ok = false; const QString region = QInputDialog::getItem(this, tr("Change Region"), tr("Region:"), regions, 0, false, &ok); if (!ok) return; applyToSelectedRooms(tr("Change Region"), [region](Room &r) { r.region = region; }); } namespace { const char *const kTrizbortMime = "application/x-trizbort"; const char *const kTrizbortColorsMime = "application/x-trizbort-colors"; // A color set copied by Copy Color, serialized to the system clipboard so it // survives across windows and app instances (the C# CopyColorsObj on the // clipboard). Colors are #AARRGGBB, or empty for "inherit the default". QString colorToken(const QColor &c) { return c.isValid() ? c.name(QColor::HexArgb) : QString(); } QString serializeRoomColors(const Room &r) { QStringList lines; lines << QStringLiteral("trizbort-colors 1"); lines << QStringLiteral("fill=") + colorToken(r.fill); lines << QStringLiteral("secondFill=") + colorToken(r.secondFill); lines << QStringLiteral("secondFillLocation=") + r.secondFillLocation; lines << QStringLiteral("border=") + colorToken(r.border); lines << QStringLiteral("largeText=") + colorToken(r.largeText); lines << QStringLiteral("subtitleColor=") + colorToken(r.subtitleColor); lines << QStringLiteral("smallText=") + colorToken(r.smallText); return lines.join(QLatin1Char('\n')); } // Parse a color set; returns false unless the leading marker is present. bool parseRoomColors(const QString &text, Room &out) { const QStringList lines = text.split(QLatin1Char('\n')); if (lines.isEmpty() || !lines.first().startsWith(QLatin1String("trizbort-colors"))) return false; auto colorFrom = [](const QString &v) { return v.isEmpty() ? QColor() : QColor(v); }; for (const QString &line : lines) { const qsizetype eq = line.indexOf(QLatin1Char('=')); if (eq < 0) continue; const QString key = line.left(eq); const QString val = line.mid(eq + 1); if (key == QLatin1String("fill")) out.fill = colorFrom(val); else if (key == QLatin1String("secondFill")) out.secondFill = colorFrom(val); else if (key == QLatin1String("secondFillLocation")) out.secondFillLocation = val.isEmpty() ? QStringLiteral("Bottom") : val; else if (key == QLatin1String("border")) out.border = colorFrom(val); else if (key == QLatin1String("largeText")) out.largeText = colorFrom(val); else if (key == QLatin1String("subtitleColor")) out.subtitleColor = colorFrom(val); else if (key == QLatin1String("smallText")) out.smallText = colorFrom(val); } return true; } } // namespace void MainWindow::copySelection() { const QList roomIds = m_scene->selectedRoomIds(); const QList connIds = m_scene->selectedConnectionIds(); if (roomIds.isEmpty() && connIds.isEmpty()) { statusBar()->showMessage(tr("Nothing selected to copy.")); return; } const QSet roomSet(roomIds.begin(), roomIds.end()); const QSet connSet(connIds.begin(), connIds.end()); Map temp; QSet regionsUsed; for (int id : roomIds) { if (const Room *r = m_map.roomById(id)) { temp.rooms.append(*r); regionsUsed.insert(r->region); } } // Copy connections explicitly selected, plus those whose docked endpoints are // all within the copied room set (a connection internal to the selection). for (const Connection &c : m_map.connections) { bool anyDock = false; bool allInside = true; for (const Vertex &v : c.vertices) { if (v.docked) { anyDock = true; if (!roomSet.contains(v.roomId)) allInside = false; } } if (connSet.contains(c.id) || (anyDock && allInside)) temp.connections.append(c); } for (const Region &rg : m_map.regions) { if (regionsUsed.contains(rg.name)) temp.regions.append(rg); } const QString xml = TrizbortWriter::toString(temp); auto *mime = new QMimeData; mime->setData(QString::fromLatin1(kTrizbortMime), xml.toUtf8()); mime->setText(xml); QApplication::clipboard()->setMimeData(mime); statusBar()->showMessage( tr("Copied %1 rooms, %2 connections.").arg(temp.rooms.size()).arg(temp.connections.size())); } void MainWindow::paste() { const QMimeData *mime = QApplication::clipboard()->mimeData(); QString xml; if (mime && mime->hasFormat(QString::fromLatin1(kTrizbortMime))) xml = QString::fromUtf8(mime->data(QString::fromLatin1(kTrizbortMime))); else if (mime && mime->hasText()) xml = mime->text(); if (xml.isEmpty()) { statusBar()->showMessage(tr("Nothing to paste.")); return; } Map temp; if (!TrizbortReader::loadFromString(xml, temp, nullptr) || (temp.rooms.isEmpty() && temp.connections.isEmpty())) { statusBar()->showMessage(tr("The clipboard does not contain map elements.")); return; } ReplaceContentCommand::Content oldContent{m_map.rooms, m_map.connections, m_map.regions}; ReplaceContentCommand::Content newContent = oldContent; int nextId = m_map.nextConnectionId(); int maxSeq = m_map.nextSeq() - 1; const double off = m_map.settings.gridSize > 1.0 ? m_map.settings.gridSize : 32.0; QHash roomIdMap; QList pastedRoomIds; for (Room r : temp.rooms) { const int newId = nextId++; roomIdMap.insert(r.id, newId); r.id = newId; r.seq = ++maxSeq; r.x += off; r.y += off; newContent.rooms.append(r); pastedRoomIds.append(newId); } for (Connection c : temp.connections) { bool ok = true; for (Vertex &v : c.vertices) { if (v.docked) { if (roomIdMap.contains(v.roomId)) v.roomId = roomIdMap.value(v.roomId); else ok = false; // endpoint outside the paste set } else { v.point += QPointF(off, off); } } if (!ok) continue; c.id = nextId++; c.seq = ++maxSeq; newContent.connections.append(c); } for (const Region &rg : temp.regions) { bool exists = false; for (const Region &e : newContent.regions) { if (e.name == rg.name) { exists = true; break; } } if (!exists) newContent.regions.append(rg); } m_undo.push(new ReplaceContentCommand(m_scene, oldContent, newContent, tr("Paste"))); m_scene->selectRoomsByIds(pastedRoomIds); statusBar()->showMessage(tr("Pasted %1 rooms.").arg(pastedRoomIds.size())); } void MainWindow::copyColor() { const int id = m_scene->selectedRoomId(); const Room *r = m_map.roomById(id); if (!r) { statusBar()->showMessage(tr("Select a room to copy its colors.")); return; } const QString text = serializeRoomColors(*r); auto *mime = new QMimeData; mime->setData(QString::fromLatin1(kTrizbortColorsMime), text.toUtf8()); mime->setText(text); QApplication::clipboard()->setMimeData(mime); statusBar()->showMessage(tr("Copied colors. Select rooms and use Paste Color (Ctrl+Alt+V).")); } void MainWindow::pasteColor() { const QMimeData *mime = QApplication::clipboard()->mimeData(); QString text; if (mime && mime->hasFormat(QString::fromLatin1(kTrizbortColorsMime))) text = QString::fromUtf8(mime->data(QString::fromLatin1(kTrizbortColorsMime))); else if (mime && mime->hasText()) text = mime->text(); Room src; if (!parseRoomColors(text, src)) { statusBar()->showMessage(tr("Copy a room's colors first (Ctrl+Alt+C).")); return; } applyToSelectedRooms(tr("Paste Color"), [src](Room &r) { r.fill = src.fill; r.secondFill = src.secondFill; r.secondFillLocation = src.secondFillLocation; r.border = src.border; r.largeText = src.largeText; r.subtitleColor = src.subtitleColor; r.smallText = src.smallText; }); } void MainWindow::selectSpecial(SelectKind kind) { if (kind == SelectKind::UnconnectedRooms || kind == SelectKind::RoomsWithObjects || kind == SelectKind::RoomsWithoutObjects) { QSet connectedRooms; for (const Connection &c : m_map.connections) for (const Vertex &v : c.vertices) if (v.docked) connectedRooms.insert(v.roomId); QList ids; for (const Room &r : m_map.rooms) { const bool hasObjects = !r.objectsText.trimmed().isEmpty(); bool sel = false; if (kind == SelectKind::UnconnectedRooms) sel = !connectedRooms.contains(r.id); else if (kind == SelectKind::RoomsWithObjects) sel = hasObjects; else sel = !hasObjects; if (sel) ids.append(r.id); } m_scene->selectRoomsByIds(ids); statusBar()->showMessage(tr("Selected %1 rooms.").arg(ids.size())); return; } QList ids; for (const Connection &c : m_map.connections) { bool sel = false; if (kind == SelectKind::AllConnections) { sel = true; } else if (kind == SelectKind::DanglingConnections) { for (const Vertex &v : c.vertices) if (!v.docked) { sel = true; break; } } else { QSet rooms; int dockCount = 0; for (const Vertex &v : c.vertices) { if (v.docked) { rooms.insert(v.roomId); ++dockCount; } } sel = (dockCount >= 2 && rooms.size() == 1); } if (sel) ids.append(c.id); } m_scene->selectConnectionsByIds(ids); statusBar()->showMessage(tr("Selected %1 connections.").arg(ids.size())); } void MainWindow::exportToClipboard(const QString &format) { auto exporter = makeExporter(format, m_map, m_filePath); if (!exporter) return; QApplication::clipboard()->setText(exporter->exportToString()); statusBar()->showMessage(tr("Copied the %1 export to the clipboard.").arg(format)); } void MainWindow::backupMap() { if (m_filePath.isEmpty()) { statusBar()->showMessage(tr("Save the map before backing it up.")); return; } const QFileInfo fi(m_filePath); for (int n = 1; n <= 9999; ++n) { const QString candidate = QStringLiteral("%1/%2 (%3).%4") .arg(fi.absolutePath(), fi.completeBaseName()) .arg(n) .arg(fi.suffix()); if (QFile::exists(candidate)) continue; if (QFile::copy(m_filePath, candidate)) statusBar()->showMessage(tr("Backed up to %1").arg(QFileInfo(candidate).fileName())); else QMessageBox::warning(this, tr("Backup Failed"), tr("Could not write %1").arg(candidate)); return; } } void MainWindow::smartSave() { // With neither artifact enabled there's nothing to smart-save (the C# // "your settings are set to not save anything" case). if (!m_smartSavePdf && !m_smartSaveImage) { QMessageBox::information( this, tr("Smart Save"), tr("Smart Save is set to write neither a PDF nor an image. Turn one on in " "Application Settings first.")); return; } // The project must be saved to a file before we can place the artifacts // next to it. if (m_filePath.isEmpty()) { if (!saveAs()) return; } else if (!save()) { return; } if (m_filePath.isEmpty()) return; const QFileInfo fi(m_filePath); const QString base = fi.absolutePath() + QLatin1Char('/') + fi.completeBaseName(); QStringList saved; QString error; bool ok = true; if (m_smartSavePdf) { if (renderMapToPdf(m_map, base + QStringLiteral(".pdf"), &error)) saved << QStringLiteral("PDF"); else ok = false; } if (m_smartSaveImage) { const QString fmt = m_smartSaveImageFormat.isEmpty() ? QStringLiteral("png") : m_smartSaveImageFormat.toLower(); const double scale = m_saveAt100 ? 1.0 : m_view->transform().m11(); if (renderMapToImage(m_map, base + QLatin1Char('.') + fmt, &error, scale)) saved << fmt.toUpper(); else ok = false; } if (!ok) statusBar()->showMessage(tr("Smart save error: %1").arg(error)); else statusBar()->showMessage(tr("Smart-saved the map and %1.").arg(saved.join(QStringLiteral(", ")))); } void MainWindow::setWatchedFile(const QString &path) { if (!m_watcher) { m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, [this](const QString &p) { // Ignore the change our own save just made. if (QDateTime::currentMSecsSinceEpoch() - m_lastSaveMs < 1500) { if (m_watcher && QFile::exists(p) && !m_watcher->files().contains(p)) m_watcher->addPath(p); return; } if (m_watcher && QFile::exists(p) && !m_watcher->files().contains(p)) m_watcher->addPath(p); reloadFromDisk(); }); } if (!m_watcher->files().isEmpty()) m_watcher->removePaths(m_watcher->files()); if (!path.isEmpty() && QFile::exists(path)) m_watcher->addPath(path); } void MainWindow::reloadFromDisk() { if (m_filePath.isEmpty() || !QFile::exists(m_filePath)) return; // Prompt like the C# TrizbortFileWatcher, warning first when a reload would // throw away unsaved edits, and letting the user discard-and-reload or keep // their version. const bool dirty = !m_undo.isClean(); const QString msg = dirty ? tr("This map has been modified by another program.\n\nReloading will discard " "your unsaved changes. Do you want to reload it?") : tr("This map has been modified by another program. Do you want to reload it?"); const auto answer = QMessageBox::question(this, tr("Reload Map?"), msg, QMessageBox::Yes | QMessageBox::No, dirty ? QMessageBox::No : QMessageBox::Yes); if (answer != QMessageBox::Yes) { statusBar()->showMessage(tr("The file changed on disk; kept your version.")); return; } const QString path = m_filePath; loadFile(path); statusBar()->showMessage( tr("Reloaded %1 after an external change.").arg(QFileInfo(path).fileName())); } void MainWindow::loadPreferences() { QSettings s; m_recentFiles = s.value(QStringLiteral("recentFiles")).toStringList(); m_loadLastOnStart = s.value(QStringLiteral("loadLastOnStart"), false).toBool(); m_showFullPath = s.value(QStringLiteral("showFullPathInTitle"), false).toBool(); m_invertWheel = s.value(QStringLiteral("invertWheelZoom"), false).toBool(); m_smartSavePdf = s.value(QStringLiteral("smartSave/pdf"), true).toBool(); m_smartSaveImage = s.value(QStringLiteral("smartSave/image"), true).toBool(); m_smartSaveImageFormat = s.value(QStringLiteral("smartSave/imageFormat"), QStringLiteral("png")).toString(); m_saveAt100 = s.value(QStringLiteral("saveImagesAt100"), true).toBool(); // Application-wide settings read by rendering code (tooltips, hand-drawn // default, port granularity, general margins, default font). AppSettings &app = AppSettings::instance(); app.showObjectsInTooltips = s.value(QStringLiteral("tooltips/showObjects"), true).toBool(); app.showDescriptionsInTooltips = s.value(QStringLiteral("tooltips/showDescriptions"), true).toBool(); app.limitRoomDescriptionChars = s.value(QStringLiteral("tooltips/limitRoomChars"), false).toBool(); app.roomDescriptionChars = s.value(QStringLiteral("tooltips/roomChars"), 50).toInt(); app.limitConnectionDescriptionChars = s.value(QStringLiteral("tooltips/limitConnectionChars"), false).toBool(); app.connectionDescriptionChars = s.value(QStringLiteral("tooltips/connectionChars"), 50).toInt(); app.handDrawnGlobal = s.value(QStringLiteral("handDrawnGlobal"), false).toBool(); app.portAdjustDetail = s.value(QStringLiteral("portAdjustDetail"), 16).toInt(); app.defaultFontName = s.value(QStringLiteral("defaultFontName"), QStringLiteral("Arial")).toString(); app.specifyGenMargins = s.value(QStringLiteral("genMargins/enabled"), false).toBool(); app.genHorizontalMargin = s.value(QStringLiteral("genMargins/horizontal"), 0.0).toDouble(); app.genVerticalMargin = s.value(QStringLiteral("genMargins/vertical"), 0.0).toDouble(); app.infiniteScrollBounds = s.value(QStringLiteral("infiniteScrollBounds"), false).toBool(); const QByteArray geometry = s.value(QStringLiteral("windowGeometry")).toByteArray(); if (!geometry.isEmpty()) restoreGeometry(geometry); } void MainWindow::savePreferences() { QSettings s; s.setValue(QStringLiteral("recentFiles"), m_recentFiles); s.setValue(QStringLiteral("loadLastOnStart"), m_loadLastOnStart); s.setValue(QStringLiteral("showFullPathInTitle"), m_showFullPath); s.setValue(QStringLiteral("invertWheelZoom"), m_invertWheel); s.setValue(QStringLiteral("smartSave/pdf"), m_smartSavePdf); s.setValue(QStringLiteral("smartSave/image"), m_smartSaveImage); s.setValue(QStringLiteral("smartSave/imageFormat"), m_smartSaveImageFormat); s.setValue(QStringLiteral("saveImagesAt100"), m_saveAt100); const AppSettings &app = AppSettings::instance(); s.setValue(QStringLiteral("tooltips/showObjects"), app.showObjectsInTooltips); s.setValue(QStringLiteral("tooltips/showDescriptions"), app.showDescriptionsInTooltips); s.setValue(QStringLiteral("tooltips/limitRoomChars"), app.limitRoomDescriptionChars); s.setValue(QStringLiteral("tooltips/roomChars"), app.roomDescriptionChars); s.setValue(QStringLiteral("tooltips/limitConnectionChars"), app.limitConnectionDescriptionChars); s.setValue(QStringLiteral("tooltips/connectionChars"), app.connectionDescriptionChars); s.setValue(QStringLiteral("handDrawnGlobal"), app.handDrawnGlobal); s.setValue(QStringLiteral("portAdjustDetail"), app.portAdjustDetail); s.setValue(QStringLiteral("defaultFontName"), app.defaultFontName); s.setValue(QStringLiteral("genMargins/enabled"), app.specifyGenMargins); s.setValue(QStringLiteral("genMargins/horizontal"), app.genHorizontalMargin); s.setValue(QStringLiteral("genMargins/vertical"), app.genVerticalMargin); s.setValue(QStringLiteral("infiniteScrollBounds"), app.infiniteScrollBounds); s.setValue(QStringLiteral("windowGeometry"), saveGeometry()); } void MainWindow::addRecentFile(const QString &path) { if (path.isEmpty()) return; const QString abs = QFileInfo(path).absoluteFilePath(); m_recentFiles.removeAll(abs); m_recentFiles.prepend(abs); while (m_recentFiles.size() > 8) m_recentFiles.removeLast(); rebuildRecentMenu(); savePreferences(); } void MainWindow::rebuildRecentMenu() { if (!m_recentMenu) return; m_recentMenu->clear(); // Prune entries whose files have gone away. for (qsizetype i = m_recentFiles.size() - 1; i >= 0; --i) if (!QFile::exists(m_recentFiles.at(i))) m_recentFiles.removeAt(i); if (m_recentFiles.isEmpty()) { QAction *none = m_recentMenu->addAction(tr("(no recent maps)")); none->setEnabled(false); return; } int n = 1; for (const QString &path : m_recentFiles) { const QString label = QStringLiteral("&%1 %2").arg(n++).arg(QFileInfo(path).fileName()); m_recentMenu->addAction(label, this, [this, path] { if (deferWhileAutomapBusy([this, path] { if (maybeSave()) loadFile(path); })) return; if (maybeSave()) loadFile(path); }); } m_recentMenu->addSeparator(); m_recentMenu->addAction(tr("&Clear List"), this, [this] { m_recentFiles.clear(); rebuildRecentMenu(); savePreferences(); }); } void MainWindow::showHelp() { // The window is created once and reused; it is non-modal so the user can // keep it open beside the map. if (!m_helpWindow) m_helpWindow = new HelpWindow(this); m_helpWindow->show(); m_helpWindow->raise(); m_helpWindow->activateWindow(); } void MainWindow::showAppSettings() { QDialog dialog(this); dialog.setWindowTitle(tr("Application Settings")); auto *form = new QFormLayout(&dialog); auto *invert = new QCheckBox(tr("Invert mouse-wheel zoom direction"), &dialog); invert->setChecked(m_invertWheel); form->addRow(invert); auto *loadLast = new QCheckBox(tr("Open the last map on start-up"), &dialog); loadLast->setChecked(m_loadLastOnStart); form->addRow(loadLast); auto *fullPath = new QCheckBox(tr("Show the full path in the title bar"), &dialog); fullPath->setChecked(m_showFullPath); form->addRow(fullPath); // Smart Save: choose which artifacts to write next to the project. auto *ssPdf = new QCheckBox(tr("Smart Save writes a PDF"), &dialog); ssPdf->setChecked(m_smartSavePdf); form->addRow(ssPdf); auto *ssImage = new QCheckBox(tr("Smart Save writes an image"), &dialog); ssImage->setChecked(m_smartSaveImage); form->addRow(ssImage); auto *ssFormat = new QComboBox(&dialog); ssFormat->addItems({QStringLiteral("png"), QStringLiteral("jpg"), QStringLiteral("bmp")}); ssFormat->setCurrentText(m_smartSaveImageFormat.toLower()); ssFormat->setEnabled(m_smartSaveImage); connect(ssImage, &QCheckBox::toggled, ssFormat, &QComboBox::setEnabled); form->addRow(tr("Smart Save image &format:"), ssFormat); auto *saveAt100 = new QCheckBox(tr("Save images at 100%"), &dialog); saveAt100->setChecked(m_saveAt100); saveAt100->setToolTip( tr("When off, exported images use the current view zoom instead of 100%.")); form->addRow(saveAt100); AppSettings &app = AppSettings::instance(); auto sectionLabel = [&](const QString &text) { auto *label = new QLabel(QStringLiteral("%1").arg(text), &dialog); form->addRow(label); }; // Tooltips. sectionLabel(tr("Tooltips")); auto *ttObjects = new QCheckBox(tr("Show objects in tooltips"), &dialog); ttObjects->setChecked(app.showObjectsInTooltips); form->addRow(ttObjects); auto *ttDesc = new QCheckBox(tr("Show descriptions in tooltips"), &dialog); ttDesc->setChecked(app.showDescriptionsInTooltips); form->addRow(ttDesc); auto *ttRoomLimit = new QCheckBox(tr("Limit room description length"), &dialog); ttRoomLimit->setChecked(app.limitRoomDescriptionChars); form->addRow(ttRoomLimit); auto *ttRoomChars = new QSpinBox(&dialog); ttRoomChars->setRange(1, 10000); ttRoomChars->setValue(app.roomDescriptionChars); ttRoomChars->setEnabled(app.limitRoomDescriptionChars); connect(ttRoomLimit, &QCheckBox::toggled, ttRoomChars, &QSpinBox::setEnabled); form->addRow(tr("Room description characters:"), ttRoomChars); auto *ttConnLimit = new QCheckBox(tr("Limit connection description length"), &dialog); ttConnLimit->setChecked(app.limitConnectionDescriptionChars); form->addRow(ttConnLimit); auto *ttConnChars = new QSpinBox(&dialog); ttConnChars->setRange(1, 10000); ttConnChars->setValue(app.connectionDescriptionChars); ttConnChars->setEnabled(app.limitConnectionDescriptionChars); connect(ttConnLimit, &QCheckBox::toggled, ttConnChars, &QSpinBox::setEnabled); form->addRow(tr("Connection description characters:"), ttConnChars); // Rendering. sectionLabel(tr("Rendering")); auto *handDrawn = new QCheckBox(tr("New rooms are hand-drawn by default"), &dialog); handDrawn->setChecked(app.handDrawnGlobal); form->addRow(handDrawn); auto *portDetail = new QComboBox(&dialog); portDetail->addItem(tr("NSEW (4)"), 4); portDetail->addItem(tr("Diagonals (8)"), 8); portDetail->addItem(tr("All ports (16)"), 16); portDetail->setCurrentIndex(app.portAdjustDetail == 4 ? 0 : app.portAdjustDetail == 8 ? 1 : 2); form->addRow(tr("Port adjustment &detail:"), portDetail); auto *defaultFont = new QFontComboBox(&dialog); if (!app.defaultFontName.isEmpty()) defaultFont->setCurrentFont(QFont(app.defaultFontName)); form->addRow(tr("Default &font:"), defaultFont); auto *infScroll = new QCheckBox(tr("Allow scrolling far beyond the map"), &dialog); infScroll->setChecked(app.infiniteScrollBounds); form->addRow(infScroll); // General export margins (used when a document has no margins of its own). sectionLabel(tr("General export margins")); auto *genMargins = new QCheckBox(tr("Use general export margins"), &dialog); genMargins->setChecked(app.specifyGenMargins); form->addRow(genMargins); auto *genH = new QDoubleSpinBox(&dialog); genH->setRange(0, 100000); genH->setValue(app.genHorizontalMargin); genH->setEnabled(app.specifyGenMargins); auto *genV = new QDoubleSpinBox(&dialog); genV->setRange(0, 100000); genV->setValue(app.genVerticalMargin); genV->setEnabled(app.specifyGenMargins); connect(genMargins, &QCheckBox::toggled, genH, &QDoubleSpinBox::setEnabled); connect(genMargins, &QCheckBox::toggled, genV, &QDoubleSpinBox::setEnabled); form->addRow(tr("Horizontal margin:"), genH); form->addRow(tr("Vertical margin:"), genV); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); form->addRow(buttons); if (dialog.exec() != QDialog::Accepted) return; m_invertWheel = invert->isChecked(); m_loadLastOnStart = loadLast->isChecked(); m_showFullPath = fullPath->isChecked(); m_smartSavePdf = ssPdf->isChecked(); m_smartSaveImage = ssImage->isChecked(); m_smartSaveImageFormat = ssFormat->currentText(); m_saveAt100 = saveAt100->isChecked(); app.showObjectsInTooltips = ttObjects->isChecked(); app.showDescriptionsInTooltips = ttDesc->isChecked(); app.limitRoomDescriptionChars = ttRoomLimit->isChecked(); app.roomDescriptionChars = ttRoomChars->value(); app.limitConnectionDescriptionChars = ttConnLimit->isChecked(); app.connectionDescriptionChars = ttConnChars->value(); app.handDrawnGlobal = handDrawn->isChecked(); app.portAdjustDetail = portDetail->currentData().toInt(); app.defaultFontName = defaultFont->currentFont().family(); app.specifyGenMargins = genMargins->isChecked(); app.genHorizontalMargin = genH->value(); app.genVerticalMargin = genV->value(); app.infiniteScrollBounds = infScroll->isChecked(); m_view->setInvertWheelZoom(m_invertWheel); updateTitle(); savePreferences(); // Tooltips and scroll bounds are derived when the scene is (re)built; // rebuild so the new settings take effect immediately. if (m_scene) m_scene->setDocument(&m_map); } void MainWindow::editMapProperties() { QDialog dialog(this); dialog.setWindowTitle(tr("Map Properties")); auto *form = new QFormLayout(&dialog); auto *title = new QLineEdit(m_map.title, &dialog); auto *author = new QLineEdit(m_map.author, &dialog); auto *description = new QPlainTextEdit(m_map.description, &dialog); description->setFixedHeight(64); auto *history = new QPlainTextEdit(m_map.history, &dialog); history->setFixedHeight(64); form->addRow(tr("&Title:"), title); form->addRow(tr("&Author:"), author); form->addRow(tr("&Description:"), description); form->addRow(tr("&History:"), history); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); form->addRow(buttons); if (dialog.exec() != QDialog::Accepted) return; EditMapInfoCommand::Info before{m_map.title, m_map.author, m_map.description, m_map.history}; EditMapInfoCommand::Info after{title->text(), author->text(), description->toPlainText(), history->toPlainText()}; if (before.title == after.title && before.author == after.author && before.description == after.description && before.history == after.history) return; m_undo.push(new EditMapInfoCommand(m_scene, before, after)); } void MainWindow::editMapSettings() { SettingsDialog dialog(m_map.settings, m_map.regions, this); if (dialog.exec() != QDialog::Accepted) return; const QHash renames = dialog.regionRenames(); const QStringList removedList = dialog.removedRegionNames(); const QSet removed(removedList.begin(), removedList.end()); m_undo.beginMacro(tr("Map Settings")); m_undo.push(new EditSettingsCommand(m_scene, m_map.settings, m_map.regions, dialog.resultSettings(), dialog.resultRegions())); // Propagate a region rename to the rooms that used it, and reassign rooms // whose region was deleted back to the default region. if (!renames.isEmpty() || !removed.isEmpty()) { QList ids; for (const Room &room : m_map.rooms) ids.append(room.id); for (int id : ids) { const Room *r = m_map.roomById(id); if (!r) continue; QString newRegion = r->region; if (renames.contains(r->region)) newRegion = renames.value(r->region); else if (removed.contains(r->region)) newRegion = kNoRegion; if (newRegion != r->region) { Room before = *r; Room after = before; after.region = newRegion; m_undo.push(new EditRoomCommand(m_scene, id, before, after)); } } } m_undo.endMacro(); } void MainWindow::updateTitle() { QString name; if (m_filePath.isEmpty()) name = tr("Untitled"); else name = m_showFullPath ? m_filePath : QFileInfo(m_filePath).fileName(); setWindowTitle(tr("%1[*] — Trizbort-qt").arg(name)); setWindowModified(!m_undo.isClean()); // Keep the grid/snap toggles in sync with the current document's settings. if (m_gridAction) m_gridAction->setChecked(m_map.settings.gridVisible); if (m_snapAction) m_snapAction->setChecked(m_map.settings.snapToGrid); } bool MainWindow::maybeSave() { if (m_automapping && !m_automapBusy) stopLiveAutomap(); if (m_undo.isClean()) return true; const auto choice = QMessageBox::warning( this, tr("Unsaved Changes"), tr("The map has unsaved changes. Save them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (choice == QMessageBox::Save) return save(); return choice == QMessageBox::Discard; } void MainWindow::closeEvent(QCloseEvent *event) { if (deferWhileAutomapBusy([this] { close(); })) { event->ignore(); return; } if (maybeSave()) { savePreferences(); event->accept(); } else { event->ignore(); } } } // namespace trizbort trizbort-qt-1.0.1/src/MainWindow.h000066400000000000000000000216351526521612000170350ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAINWINDOW_H #define TRIZBORT_MAINWINDOW_H #include #include #include #include "MapDocument.h" class QAction; class QDockWidget; class QFileSystemWatcher; class QLabel; class QMenu; class QPushButton; class QTimer; namespace trizbort { class MapScene; class MapView; class TranscriptAutomapper; class GuiAutomapController; class HelpWindow; enum class SelectKind { UnconnectedRooms, RoomsWithObjects, RoomsWithoutObjects, AllConnections, DanglingConnections, SelfLoopingConnections }; enum class SwapMode { Objects, Names, Formats, Regions }; class MainWindow : public QMainWindow { Q_OBJECT friend int runKeyboardSelftest(); friend int driveGuiOverMap(const QString &path); public: explicit MainWindow(QWidget *parent = nullptr); bool loadFile(const QString &path); protected: void closeEvent(QCloseEvent *event) override; // Keyboard editing/navigation on the canvas (the C# Canvas key handlers): // arrow-key nudge/scroll, Ctrl+arrow select-or-add, Shift+arrow follow a // connection, Ctrl+Alt+arrow resize, and numeric-keypad navigation. bool eventFilter(QObject *obj, QEvent *event) override; private slots: void newFile(); void openFile(); void openFromUrl(); bool save(); bool saveAs(); void exportMap(const QString &format); void exportImage(); void exportPdf(); void importTranscript(); void startLiveAutomap(); void stopLiveAutomap(); void abandonLiveAutomap(); // tear down without recording an undo step void automapTick(); void addRoom(); void addConnectedRoom(const QString &direction); // Keyboard-editing helpers (see eventFilter). Directions are compass tokens // ("n","s","e","w","ne","nw","se","sw"). bool handleCanvasKey(QKeyEvent *event); // Follow a connection docked at the selected room toward the given compass // direction (exact, then ±45°) and return the room at the far end, or -1. int roomThroughConnection(int roomId, const QString &dir) const; void nudgeSelection(double dx, double dy); // move selected elements void keyboardResizeRooms(const QString &dir); // Ctrl+Alt+arrow // Select the room reached through a connection in `dir`; when none exists, // optionally add a connected room, or add an unexplored dangling exit. void navigateOrAdd(const QString &dir, bool allowCreate, bool unexploredStub); void addUnexploredExit(int roomId, const QString &dir); void addConnectedRoomLabeled(const QString &placementDir, const QString &startLabel, const QString &endLabel, const QString &displayName); void deleteSelection(); void toggleConnectMode(bool on); void editMapProperties(); void editMapSettings(); void editRoom(int roomId); void editConnection(int connId); private: // Apply an edit to every selected room as one undoable step. void applyToSelectedRooms(const QString &label, const std::function &fn); void applyToSelectedConnections(const QString &label, const std::function &fn); void reverseSelectedConnections(); void rotateSelectedConnectors(bool source); void makeSelectedConnectionsOrthogonal(); bool orthogonalizeConnection(Connection &c) const; void insertRoomOnConnection(); void deleteConnectionsOfSelectedRoom(); void setStartOrEndRoom(bool start); void setSelectedRoomShape(RoomShape shape); void setSelectedRoomsZOrder(bool toFront); void joinSelectedRooms(); void swapSelectedRooms(SwapMode mode); void renameSelectedRoom(); void changeSelectedRegion(); void selectRoomsInSelectedRegions(); void showCanvasContextMenu(const QPointF &scenePos, const QPoint &screenPos); bool deferWhileAutomapBusy(const std::function &retry); // Clipboard. void copySelection(); void paste(); void copyColor(); void pasteColor(); // Export / file helpers. void exportToClipboard(const QString &format); void backupMap(); void smartSave(); void setWatchedFile(const QString &path); void reloadFromDisk(); // Application preferences and recent-files (persisted via QSettings). void loadPreferences(); void savePreferences(); void addRecentFile(const QString &path); void rebuildRecentMenu(); void showAppSettings(); void showHelp(); // open the built-in help window (F1) void selectSpecial(SelectKind kind); void createActions(); void updateTitle(); bool maybeSave(); // returns false to cancel the pending action bool writeToPath(const QString &path); Map m_map; QString m_filePath; QUndoStack m_undo; QFileSystemWatcher *m_watcher = nullptr; qint64 m_lastSaveMs = 0; // ignore watcher events right after our own save // Live automap: an incremental, controller-driven engine fed the transcript // as it grows on disk. QTimer *m_automapTimer = nullptr; QString m_automapPath; qint64 m_automapOffset = 0; QByteArray m_automapPartialLine; Map m_automapPreMap; // map content captured when automap started QAction *m_automapStopAction = nullptr; QPushButton *m_automapStopButton = nullptr; QAction *m_automapStepAction = nullptr; QAction *m_automapRunAction = nullptr; bool m_automapping = false; bool m_automapBusy = false; // guard against re-entrant ticks while stepping bool m_automapStopRequested = false; bool m_automapAbandonRequested = false; TranscriptAutomapper *m_automapper = nullptr; GuiAutomapController *m_automapController = nullptr; MapScene *m_scene = nullptr; MapView *m_view = nullptr; QAction *m_connectAction = nullptr; QAction *m_gridAction = nullptr; QAction *m_snapAction = nullptr; QDockWidget *m_minimapDock = nullptr; HelpWindow *m_helpWindow = nullptr; // built-in help, created on first use QLabel *m_zoomLabel = nullptr; QMenu *m_recentMenu = nullptr; QStringList m_recentFiles; bool m_loadLastOnStart = false; bool m_showFullPath = false; bool m_invertWheel = false; // Smart Save: which artifacts to write alongside the project (the C# // SaveToPDF / SaveToImage app settings), and the image format to use. bool m_smartSavePdf = true; bool m_smartSaveImage = true; QString m_smartSaveImageFormat = QStringLiteral("png"); // When true, image/PDF exports render at 100% (1 unit per pixel); when false // they render at the current view zoom (the C# SaveAt100 app setting). bool m_saveAt100 = true; }; } // namespace trizbort #endif // TRIZBORT_MAINWINDOW_H trizbort-qt-1.0.1/src/MapDocument.cpp000066400000000000000000000272561526521612000175350ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "MapDocument.h" #include #include "AppSettings.h" namespace trizbort { QString normalizeRegionName(const QString &name) { return name.isEmpty() ? kNoRegion : name; } double clampCornerRadius(double radius) { return std::clamp(radius, kMinCornerRadius, kMaxCornerRadius); } bool operator==(const ObjectsPosition &a, const ObjectsPosition &b) { return a.at == b.at && a.custom == b.custom && a.customRight == b.customRight && a.customDown == b.customDown; } bool operator==(const Room &a, const Room &b) { return a.id == b.id && a.seq == b.seq && a.name == b.name && a.subtitle == b.subtitle && a.description == b.description && a.region == b.region && a.referenceRoom == b.referenceRoom && a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h && a.handDrawn == b.handDrawn && a.allCornersEqual == b.allCornersEqual && a.ellipse == b.ellipse && a.roundedCorners == b.roundedCorners && a.octagonal == b.octagonal && a.cornerTopLeft == b.cornerTopLeft && a.cornerTopRight == b.cornerTopRight && a.cornerBottomLeft == b.cornerBottomLeft && a.cornerBottomRight == b.cornerBottomRight && a.borderStyle == b.borderStyle && a.isDark == b.isDark && a.isStartRoom == b.isStartRoom && a.isEndRoom == b.isEndRoom && a.fill == b.fill && a.secondFill == b.secondFill && a.secondFillLocation == b.secondFillLocation && a.border == b.border && a.largeText == b.largeText && a.subtitleColor == b.subtitleColor && a.smallText == b.smallText && a.zOrder == b.zOrder && a.objectsText == b.objectsText && a.objectsPosition == b.objectsPosition; } bool operator==(const Door &a, const Door &b) { return a.lockable == b.lockable && a.locked == b.locked && a.open == b.open && a.openable == b.openable; } bool operator==(const Vertex &a, const Vertex &b) { return a.index == b.index && a.docked == b.docked && a.roomId == b.roomId && a.port == b.port && a.point == b.point; } bool operator==(const Connection &a, const Connection &b) { return a.id == b.id && a.seq == b.seq && a.style == b.style && a.flow == b.flow && a.name == b.name && a.description == b.description && a.startText == b.startText && a.midText == b.midText && a.endText == b.endText && a.color == b.color && a.hasDoor == b.hasDoor && a.door == b.door && a.vertices == b.vertices; } // Default palette, fonts and geometry, matching Settings.Reset() in the C#. MapSettings::MapSettings() { colors[ColorCanvas] = QColor(255, 255, 255); // White colors[ColorBorder] = QColor(25, 25, 112); // MidnightBlue colors[ColorLine] = QColor(25, 25, 112); // MidnightBlue colors[ColorSelectedLine] = QColor(255, 215, 0); // Gold colors[ColorHoverLine] = QColor(255, 140, 0); // DarkOrange colors[ColorSubtitle] = QColor(25, 25, 112); // MidnightBlue colors[ColorSmallText] = QColor(25, 25, 112); // MidnightBlue colors[ColorLineText] = QColor(25, 25, 112); // MidnightBlue colors[ColorGrid] = QColor(232, 232, 232); // Mix(White, Black, 10, 1) colors[ColorStartRoom] = QColor(173, 255, 47); // GreenYellow colors[ColorEndRoom] = QColor(255, 0, 0); // Red // Default serialized tokens match what .NET's ColorTranslator.ToHtml emits // for the Reset() palette: the KnownColor names, and hex for the computed // grid color (Mix, which is not a known color). colorTokens[ColorCanvas] = QStringLiteral("White"); colorTokens[ColorBorder] = QStringLiteral("MidnightBlue"); colorTokens[ColorLine] = QStringLiteral("MidnightBlue"); colorTokens[ColorSelectedLine] = QStringLiteral("Gold"); colorTokens[ColorHoverLine] = QStringLiteral("DarkOrange"); colorTokens[ColorSubtitle] = QStringLiteral("MidnightBlue"); colorTokens[ColorSmallText] = QStringLiteral("MidnightBlue"); colorTokens[ColorLineText] = QStringLiteral("MidnightBlue"); colorTokens[ColorGrid] = QStringLiteral("#E8E8E8"); colorTokens[ColorStartRoom] = QStringLiteral("GreenYellow"); colorTokens[ColorEndRoom] = QStringLiteral("Red"); const QString family = QStringLiteral("Arial"); roomFont = FontSpec{family, 13.0, false, false, false, false}; objectFont = FontSpec{family, 11.0, false, false, false, false}; subtitleFont = FontSpec{family, 9.0, false, false, false, false}; lineFont = FontSpec{family, 9.0, false, false, false, false}; } void Map::clear() { version.clear(); title.clear(); author.clear(); description.clear(); history.clear(); rooms.clear(); connections.clear(); regions.clear(); settings = MapSettings(); m_roomIndex.clear(); } void Map::reindex() { m_roomIndex.clear(); for (int i = 0; i < rooms.size(); ++i) m_roomIndex.insert(rooms.at(i).id, i); } const Room *Map::roomById(int id) const { const auto it = m_roomIndex.constFind(id); if (it == m_roomIndex.constEnd()) return nullptr; return &rooms.at(it.value()); } Room *Map::roomById(int id) { const auto it = m_roomIndex.constFind(id); if (it == m_roomIndex.constEnd()) return nullptr; return &rooms[it.value()]; } const Region *Map::regionByName(const QString &name) const { for (const Region &r : regions) { if (r.name == name) return &r; } return nullptr; } QColor Map::regionFill(const QString &name) const { if (name.isEmpty() || name == kNoRegion) return QColor(); if (const Region *r = regionByName(name)) return r->fillColor; return QColor(); } int Map::nextRoomId() const { // Rooms and connections share one id space in the format, so a new id must // clear both. return nextConnectionId(); } int Map::nextConnectionId() const { int maxId = 0; for (const Room &r : rooms) maxId = qMax(maxId, r.id); for (const Connection &c : connections) maxId = qMax(maxId, c.id); return maxId + 1; } QColor Map::canvasColor() const { return settings.colors[ColorCanvas]; } QColor Map::lineColor() const { return settings.colors[ColorLine]; } QColor Map::borderColor() const { return settings.colors[ColorBorder]; } int Map::nextSeq() const { int maxSeq = 0; for (const Room &r : rooms) maxSeq = qMax(maxSeq, r.seq); for (const Connection &c : connections) maxSeq = qMax(maxSeq, c.seq); return maxSeq + 1; } int Map::highestZOrder() const { if (rooms.isEmpty()) return 0; int high = rooms.first().zOrder; for (const Room &r : rooms) high = qMax(high, r.zOrder); return high + 1; } int Map::lowestZOrder() const { if (rooms.isEmpty()) return 0; int low = rooms.first().zOrder; for (const Room &r : rooms) low = qMin(low, r.zOrder); return low - 1; } int Map::addRoom(double x, double y) { Room r; r.id = nextRoomId(); r.seq = nextSeq(); r.name = settings.defaultRoomName; r.x = x; r.y = y; // The document's default room shape maps onto the boolean shape flags. switch (static_cast(settings.defaultRoomShape)) { case RoomShape::RoundedCorners: r.roundedCorners = true; break; case RoomShape::Ellipse: r.ellipse = true; break; case RoomShape::Octagonal: r.octagonal = true; break; default: break; } // New rooms inherit the application's global hand-drawn default. r.handDrawn = AppSettings::instance().handDrawnGlobal; rooms.append(r); reindex(); return r.id; } void Map::removeRoom(int roomId) { for (qsizetype i = connections.size() - 1; i >= 0; --i) { for (const Vertex &v : connections.at(i).vertices) { if (v.docked && v.roomId == roomId) { connections.removeAt(i); break; } } } for (int i = 0; i < rooms.size(); ++i) { if (rooms.at(i).id == roomId) { rooms.removeAt(i); break; } } reindex(); } int Map::addConnection(int roomA, const QString &portA, int roomB, const QString &portB) { Connection c; c.id = nextConnectionId(); c.seq = nextSeq(); Vertex a; a.index = 0; a.docked = true; a.roomId = roomA; a.port = portA; Vertex b; b.index = 1; b.docked = true; b.roomId = roomB; b.port = portB; c.vertices.append(a); c.vertices.append(b); connections.append(c); return c.id; } int Map::connectionIndex(int connId) const { for (int i = 0; i < connections.size(); ++i) if (connections.at(i).id == connId) return i; return -1; } void Map::removeConnection(int connId) { const int i = connectionIndex(connId); if (i >= 0) connections.removeAt(i); } QString Map::portFacing(const Room &from, const Room &to) { const double dx = (to.x + to.w / 2.0) - (from.x + from.w / 2.0); const double dy = (to.y + to.h / 2.0) - (from.y + from.h / 2.0); // Screen coordinates: +y is down. Classify into 8 compass ports by octant. const double adx = qAbs(dx); const double ady = qAbs(dy); const bool horizontal = adx > ady * 2.0; const bool vertical = ady > adx * 2.0; if (vertical) return dy < 0 ? QStringLiteral("n") : QStringLiteral("s"); if (horizontal) return dx < 0 ? QStringLiteral("w") : QStringLiteral("e"); if (dx >= 0 && dy < 0) return QStringLiteral("ne"); if (dx >= 0 && dy >= 0) return QStringLiteral("se"); if (dx < 0 && dy < 0) return QStringLiteral("nw"); return QStringLiteral("sw"); } } // namespace trizbort trizbort-qt-1.0.1/src/MapDocument.h000066400000000000000000000276311526521612000171770ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAPDOCUMENT_H #define TRIZBORT_MAPDOCUMENT_H #include #include #include #include #include #include // Complete in-memory model of a .trizbort map. Every element and attribute the // Trizbort file format defines is modeled here as a typed field; nothing is // dropped or preserved as opaque text. See the C# originals // (Domain/Elements/Room.cs, Connection.cs, Setup/Settings.cs) which this // mirrors. namespace trizbort { // The default region name; rooms and the region list use it for "no region". inline const QString kNoRegion = QStringLiteral("NoRegion"); inline const QString kFileFormatVersion = QStringLiteral("1.8.0.0"); inline constexpr double kPi = 3.14159265358979323846; inline constexpr double kMinCornerRadius = 1.0; inline constexpr double kMaxCornerRadius = 30.0; inline constexpr double kDefaultCornerRadius = 15.0; inline constexpr double kMinFontSize = 2.0; inline constexpr double kMaxFontSize = 256.0; // A room's shape, matching the C# RoomShape enum (persisted as an int in // ). enum class RoomShape { SquareCorners, RoundedCorners, Ellipse, Octagonal }; QString normalizeRegionName(const QString &name); double clampCornerRadius(double radius); struct Region { QString name; QColor textColor; // invalid => default (Blue) QColor fillColor; // invalid => default (White) // The exact serialized token each color was read from (e.g. "#F5F5F5" vs // "WhiteSmoke"), so an unedited color is written back byte-for-byte. Empty // => serialize the QColor via ColorTranslator.ToHtml. QString textColorToken; QString fillColorToken; }; // Where a room's object list is anchored, persisted as a compass token in the // attribute. South is the default and is omitted on save. struct ObjectsPosition { QString at; // compass token ("n","sw",...); empty => default (South) bool custom = false; int customRight = 0; int customDown = 0; }; struct Room { int id = 0; int seq = 0; // position among all elements, to preserve file order QString name; QString subtitle; QString description; QString region = kNoRegion; // region name; "NoRegion" => none int referenceRoom = -1; // id of the room this references, or -1 double x = 0.0; // top-left corner, map units double y = 0.0; double w = 96.0; double h = 64.0; // Shape / border. bool handDrawn = false; bool allCornersEqual = false; bool ellipse = false; bool roundedCorners = false; bool octagonal = false; double cornerTopLeft = kDefaultCornerRadius; double cornerTopRight = kDefaultCornerRadius; double cornerBottomLeft = kDefaultCornerRadius; double cornerBottomRight = kDefaultCornerRadius; QString borderStyle = QStringLiteral("Solid"); // Solid | Dashed | None | Dot | ... // Flags. bool isDark = false; bool isStartRoom = false; bool isEndRoom = false; // Colors (invalid => inherit the map/settings default). QColor fill; QColor secondFill; QString secondFillLocation = QStringLiteral("Bottom"); QColor border; QColor largeText; // roomLargeText QColor subtitleColor; // roomSubtitleColor QColor smallText; // roomSmallText int zOrder = 0; // Objects: the raw list text (indentation denotes containment, [..] encodes // properties) with newlines as real '\n'; plus where the list is anchored. QString objectsText; ObjectsPosition objectsPosition; }; bool operator==(const ObjectsPosition &a, const ObjectsPosition &b); bool operator==(const Room &a, const Room &b); inline bool operator!=(const Room &a, const Room &b) { return !(a == b); } enum class ConnectionStyle { Solid, Dashed }; enum class ConnectionFlow { TwoWay, OneWay }; struct Door { bool lockable = false; bool locked = false; bool open = false; bool openable = false; }; struct Vertex { int index = 0; bool docked = false; // true => (roomId, port); false => point int roomId = 0; QString port; // "n","s","e","w","ne","nw","se","sw",... QPointF point; // free vertex, map units }; struct Connection { int id = 0; int seq = 0; // position among all elements, to preserve file order ConnectionStyle style = ConnectionStyle::Solid; ConnectionFlow flow = ConnectionFlow::TwoWay; QString name; QString description; QString startText; QString midText; QString endText; QColor color; // invalid => inherit map line color bool hasDoor = false; Door door; QList vertices; // ordered; >= 2 endpoints when valid }; bool operator==(const Door &a, const Door &b); bool operator==(const Vertex &a, const Vertex &b); bool operator==(const Connection &a, const Connection &b); inline bool operator!=(const Connection &a, const Connection &b) { return !(a == b); } // A font as stored in : family plus size and style flags. struct FontSpec { QString family; double size = 0.0; bool bold = false; bool italic = false; bool underline = false; bool strikeout = false; }; // Which slot each color occupies in , matching Colors.cs. // The order is the persisted order (Colors.Names). enum ColorSlot { ColorCanvas = 0, ColorBorder, ColorLine, ColorSelectedLine, ColorHoverLine, ColorSubtitle, ColorSmallText, ColorLineText, ColorGrid, ColorStartRoom, ColorEndRoom, ColorCount }; // The full block: per-map when saved, mirroring Setup/Settings.cs. struct MapSettings { QColor colors[ColorCount]; // indexed by ColorSlot; invalid => default // Exact serialized token for each palette color (see Region), so a color // Trizbort stored as hex is not rewritten as its equivalent name and back. QString colorTokens[ColorCount]; FontSpec roomFont; // FontSpec objectFont; // FontSpec subtitleFont; // FontSpec lineFont; // bool snapToGrid = true; bool gridVisible = true; bool showOrigin = true; double gridSize = 32.0; double lineWidth = 2.0; double arrowSize = 12.0; double textOffset = 4.0; double darknessStripeSize = 24.0; double objectListOffset = 4.0; double connectionStalkLength = 32.0; double preferredDistanceBetweenRooms = 64.0; QString defaultRoomName = QStringLiteral("Cave"); int defaultRoomShape = 0; // RoomShape as int double handleSize = 12.0; double snapToElementSize = 16.0; bool documentSpecificMargins = false; double horizontalMargin = 0.0; double verticalMargin = 0.0; bool wrapTextAtDashes = true; QString keypadCreationModifier = QStringLiteral("control"); QString keypadUnexploredModifier = QStringLiteral("alt"); // True once a settings block has actually been read from a file, so a // freshly-created document can be told apart from a loaded one. bool loaded = false; // Legacy map-wide "hand-drawn edges" flag () used by // pre-per-room files. On load it is applied to every room when the file // carries no explicit per-room handDrawn attribute (see TrizbortReader), so // an old globally-hand-drawn map keeps its look; it is not written back (the // modern format is per-room). Both fields are load-time state only. bool handDrawnGlobal = false; bool anyRoomHandDrawnExplicit = false; MapSettings(); }; class Map { public: QString version; QString title; QString author; QString description; QString history; QList rooms; QList connections; QList regions; MapSettings settings; void clear(); void reindex(); // rebuild id -> room lookup const Room *roomById(int id) const; // nullptr if not found Room *roomById(int id); QColor regionFill(const QString &name) const; // invalid if unknown const Region *regionByName(const QString &name) const; int nextRoomId() const; // smallest unused positive id int nextConnectionId() const; int nextSeq() const; // element order for a new element int highestZOrder() const; int lowestZOrder() const; // --- Editing operations (GUI-independent). Structural changes keep the // id index consistent; returned pointers are invalidated by later edits, so // re-fetch by id after any further mutation. --- // Add a room at (x, y) with the document's default name/size. Returns its id. int addRoom(double x, double y); // Remove a room and every connection docked to it. void removeRoom(int roomId); // Add a two-way connection docked between two rooms' ports. Returns its id. int addConnection(int roomA, const QString &portA, int roomB, const QString &portB); // Remove a connection by id. void removeConnection(int connId); // Connection index by id, or -1. int connectionIndex(int connId) const; // The dock port on "from" that best faces "to" (one of n/s/e/w/ne/nw/se/sw). static QString portFacing(const Room &from, const Room &to); // Convenience accessors for the three colors the renderer reads most; these // resolve to the settings palette (ColorCanvas / ColorLine / ColorBorder). QColor canvasColor() const; QColor lineColor() const; QColor borderColor() const; private: QHash m_roomIndex; // room id -> index in rooms }; } // namespace trizbort #endif // TRIZBORT_MAPDOCUMENT_H trizbort-qt-1.0.1/src/MapRender.cpp000066400000000000000000000126501526521612000171660ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "MapRender.h" #include #include #include #include #include #include #include "AppSettings.h" #include "MapScene.h" namespace trizbort { namespace { QRectF contentRect(QGraphicsScene &scene, const Map &map) { QRectF r = scene.itemsBoundingRect(); if (r.isEmpty()) r = QRectF(0, 0, 200, 200); // Honor document-specific page margins when set; else the application's // general margins when enabled; otherwise a small default gutter so nothing // is clipped at the edge. if (map.settings.documentSpecificMargins) { const double hm = map.settings.horizontalMargin; const double vm = map.settings.verticalMargin; return r.adjusted(-hm, -vm, hm, vm); } const AppSettings &app = AppSettings::instance(); if (app.specifyGenMargins) { return r.adjusted(-app.genHorizontalMargin, -app.genVerticalMargin, app.genHorizontalMargin, app.genVerticalMargin); } return r.adjusted(-24, -24, 24, 24); } QColor canvas(const Map &map) { const QColor c = map.canvasColor(); return c.isValid() ? c : QColor(Qt::white); } } // namespace bool renderMapToImage(const Map &map, const QString &path, QString *errorMessage, double scale) { // Render off a copy with the grid suppressed, so the export is clean. Map copy = map; copy.settings.gridVisible = false; MapScene scene; scene.setDocument(©); const QRectF bounds = contentRect(scene, copy); if (!(scale > 0.0)) scale = 1.0; const QSize size = (bounds.size() * scale).toSize(); QImage image(size, QImage::Format_ARGB32); image.fill(canvas(copy)); { QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); scene.render(&painter, QRectF(QPointF(0, 0), image.size()), bounds); } if (!image.save(path)) { if (errorMessage) *errorMessage = QObject::tr("Could not write image %1").arg(path); return false; } return true; } bool renderMapToPdf(const Map &map, const QString &path, QString *errorMessage) { Map copy = map; copy.settings.gridVisible = false; MapScene scene; scene.setDocument(©); const QRectF bounds = contentRect(scene, copy); QPdfWriter writer(path); writer.setResolution(72); // 1 map unit == 1 PostScript point writer.setPageSize(QPageSize(QSizeF(bounds.width(), bounds.height()), QPageSize::Point)); writer.setPageMargins(QMarginsF(0, 0, 0, 0)); writer.setTitle(map.title.isEmpty() ? QStringLiteral("Trizbort map") : map.title); QPainter painter; if (!painter.begin(&writer)) { if (errorMessage) *errorMessage = QObject::tr("Could not open %1 for PDF output").arg(path); return false; } painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); const QRectF target(0, 0, writer.width(), writer.height()); painter.fillRect(target, canvas(copy)); scene.render(&painter, target, bounds); painter.end(); return true; } } // namespace trizbort trizbort-qt-1.0.1/src/MapRender.h000066400000000000000000000057711526521612000166410ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAPRENDER_H #define TRIZBORT_MAPRENDER_H #include #include "MapDocument.h" // Renders a map to a raster image or a PDF, using the same drawing the editor // canvas uses (minus the grid). Used by the File > Export menu and the CLI. namespace trizbort { // scale multiplies the output resolution: 1.0 renders the map at 100% (one map // unit per pixel), matching the "Save images at 100%" behavior; the GUI passes // the current view zoom instead when that option is off. bool renderMapToImage(const Map &map, const QString &path, QString *errorMessage = nullptr, double scale = 1.0); bool renderMapToPdf(const Map &map, const QString &path, QString *errorMessage = nullptr); } // namespace trizbort #endif // TRIZBORT_MAPRENDER_H trizbort-qt-1.0.1/src/MapScene.cpp000066400000000000000000000657231526521612000170150ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "MapScene.h" #include #include #include #include #include #include #include #include #include "AppSettings.h" #include "ConnectionItem.h" #include "EditCommands.h" #include "RoomItem.h" namespace trizbort { MapScene::MapScene(QObject *parent) : QGraphicsScene(parent) { connect(this, &QGraphicsScene::selectionChanged, this, [this] { if (m_clearing) return; for (ConnectionItem *item : std::as_const(m_connItems)) item->clearHover(); }); } MapScene::~MapScene() { m_clearing = true; disconnect(this, &QGraphicsScene::selectionChanged, nullptr, nullptr); m_roomItems.clear(); m_connItems.clear(); clear(); } double MapScene::gridSize() const { if (m_map && m_map->settings.gridSize > 1.0) return m_map->settings.gridSize; return 32.0; } QPointF MapScene::snap(const QPointF &p) const { if (m_map && !m_map->settings.snapToGrid) return p; const double g = gridSize(); return QPointF(std::round(p.x() / g) * g, std::round(p.y() / g) * g); } namespace { constexpr double kDragToConnectPixels = 32.0; // The compass index (0..15) used to place a port on an ellipse, matching // CompassPointHelper.getPointIntegerValue: East is 0 and the index advances // clockwise. Returns -1 for an unknown token. int ellipseCompassIndex(const QString &p) { static const QHash kIndex = { {"e", 0}, {"ese", 1}, {"se", 2}, {"sse", 3}, {"s", 4}, {"ssw", 5}, {"sw", 6}, {"wsw", 7}, {"w", 8}, {"wnw", 9}, {"nw", 10}, {"nnw", 11}, {"n", 12}, {"nne", 13},{"ne", 14}, {"ene", 15}, }; return kIndex.value(p, -1); } } // namespace QPointF MapScene::squareCorner(double x, double y, double w, double h, const QString &port) { // Fractions of (w, h) for each of the sixteen compass ports on a rectangle, // matching Rect.GetCorner (square branch). double fx = 0.5, fy = 0.5; const QString p = port.toLower(); if (p == QLatin1String("n")) { fx = 0.5; fy = 0.0; } else if (p == QLatin1String("nne")) { fx = 0.75; fy = 0.0; } else if (p == QLatin1String("ne")) { fx = 1.0; fy = 0.0; } else if (p == QLatin1String("ene")) { fx = 1.0; fy = 0.25; } else if (p == QLatin1String("e")) { fx = 1.0; fy = 0.5; } else if (p == QLatin1String("ese")) { fx = 1.0; fy = 0.75; } else if (p == QLatin1String("se")) { fx = 1.0; fy = 1.0; } else if (p == QLatin1String("sse")) { fx = 0.75; fy = 1.0; } else if (p == QLatin1String("s")) { fx = 0.5; fy = 1.0; } else if (p == QLatin1String("ssw")) { fx = 0.25; fy = 1.0; } else if (p == QLatin1String("sw")) { fx = 0.0; fy = 1.0; } else if (p == QLatin1String("wsw")) { fx = 0.0; fy = 0.75; } else if (p == QLatin1String("w")) { fx = 0.0; fy = 0.5; } else if (p == QLatin1String("wnw")) { fx = 0.0; fy = 0.25; } else if (p == QLatin1String("nw")) { fx = 0.0; fy = 0.0; } else if (p == QLatin1String("nnw")) { fx = 0.25; fy = 0.0; } // else: center (fx = fy = 0.5), matching the C# unknown-port fallback. return QPointF(x + w * fx, y + h * fy); } QPointF MapScene::portPoint(const Room &room, const QString &port) { const double x = room.x; const double y = room.y; const double w = room.w; const double h = room.h; const QString p = port.toLower(); if (room.ellipse) { const int i = ellipseCompassIndex(p); if (i < 0) return QPointF(x + w / 2.0, y + h / 2.0); const double theta = i * (2.0 * kPi / 16.0); return QPointF(x + w / 2.0 + (w / 2.0) * std::cos(theta), y + h / 2.0 + (h / 2.0) * std::sin(theta)); } if (room.octagonal) { // The four ordinals are chamfered to 1/8 insets; the rest lie on the // bounding rectangle exactly as a square room's do. if (p == QLatin1String("ne")) return {x + w * 7.0 / 8.0, y + h * 1.0 / 8.0}; if (p == QLatin1String("se")) return {x + w * 7.0 / 8.0, y + h * 7.0 / 8.0}; if (p == QLatin1String("sw")) return {x + w * 1.0 / 8.0, y + h * 7.0 / 8.0}; if (p == QLatin1String("nw")) return {x + w * 1.0 / 8.0, y + h * 1.0 / 8.0}; } return squareCorner(x, y, w, h, p); } QPointF MapScene::portStalkPoint(const Room &room, const QString &port, double stalk) { if (stalk <= 0.0) return portPoint(room, port); // Mirrors Room.GetPortStalkPosition: the inner corner is taken on the room // itself, the outer corner on the room inflated by the stalk length, and the // two are combined so the eight "half" ports run a straight stalk that stays // perpendicular to the room edge. const QPointF inner = squareCorner(room.x, room.y, room.w, room.h, port); const QPointF outer = squareCorner(room.x - stalk, room.y - stalk, room.w + 2.0 * stalk, room.h + 2.0 * stalk, port); const QString p = port.toLower(); if (p == QLatin1String("ene") || p == QLatin1String("ese") || p == QLatin1String("wnw") || p == QLatin1String("wsw")) return {outer.x(), inner.y()}; if (p == QLatin1String("nne") || p == QLatin1String("nnw") || p == QLatin1String("sse") || p == QLatin1String("ssw")) return {inner.x(), outer.y()}; return outer; } QString MapScene::portTowards(const Room &room, const QPointF &target) { static const char *const kPorts[16] = {"n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"}; const double cx = room.x + room.w / 2.0; const double cy = room.y + room.h / 2.0; const double dx = target.x() - cx; const double dy = target.y() - cy; if (dx == 0.0 && dy == 0.0) return QStringLiteral("n"); // How many compass points to snap to (the C# PortAdjustDetail: 4/8/16). int divisions = AppSettings::instance().portAdjustDetail; if (divisions != 4 && divisions != 8) divisions = 16; // Angle clockwise from north (screen y grows downward), bucketed to the // nearest of `divisions` evenly-spaced compass points. constexpr double kTwoPi = 2.0 * kPi; double angle = std::atan2(dx, -dy); if (angle < 0.0) angle += kTwoPi; const int bucket = static_cast(std::llround(angle / (kTwoPi / divisions))) % divisions; return QString::fromLatin1(kPorts[(bucket * (16 / divisions)) % 16]); } QStringList MapScene::portTokensForDetail(int detail) { static const char *const k16[16] = {"n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"}; if (detail != 4 && detail != 8 && detail != 16) detail = 16; QStringList out; for (int i = 0; i < 16; i += 16 / detail) out << QString::fromLatin1(k16[i]); return out; } QString MapScene::nearestPort(const Room &room, const QPointF &scenePoint, int detail) { const QStringList tokens = portTokensForDetail(detail); QString best = tokens.isEmpty() ? QStringLiteral("n") : tokens.first(); double bestD = 1e18; for (const QString &t : tokens) { const QPointF p = portPoint(room, t); const double d = std::hypot(p.x() - scenePoint.x(), p.y() - scenePoint.y()); if (d < bestD) { bestD = d; best = t; } } return best; } int MapScene::roomIdAt(const QPointF &scenePos) const { if (RoomItem *item = roomItemAt(scenePos)) return item->roomId(); return -1; } int MapScene::roomNearestWithin(const QPointF &scenePos, double maxDist) const { if (!m_map) return -1; int best = -1; double bestDist = maxDist; for (const Room &r : m_map->rooms) { const QRectF rect(r.x, r.y, r.w, r.h); // Distance from the point to the room rectangle (0 when inside). const double dx = std::max({rect.left() - scenePos.x(), 0.0, scenePos.x() - rect.right()}); const double dy = std::max({rect.top() - scenePos.y(), 0.0, scenePos.y() - rect.bottom()}); const double dist = std::hypot(dx, dy); if (dist <= bestDist) { bestDist = dist; best = r.id; } } return best; } QVector> MapScene::connectionSegmentsNear(const ConnectionItem *item) const { QVector> segs; if (!item) return segs; const QList nearby = items(item->sceneBoundingRect()); for (QGraphicsItem *g : nearby) { const auto *other = dynamic_cast(g); if (!other || other == item || other->connId() >= item->connId()) continue; const QVector &pts = other->routePoints(); for (int i = 1; i < pts.size(); ++i) segs.append({QLineF(pts.at(i - 1), pts.at(i)), other->connId()}); } return segs; } void MapScene::setDocument(Map *map) { m_map = map; rebuild(); } void MapScene::rebuild() { m_roomItems.clear(); m_connItems.clear(); m_clearing = true; clear(); // deletes all items, including any rubber-band line m_clearing = false; m_rubberLine = nullptr; m_connectFromRoom = -1; m_validationCacheDirty = true; if (!m_map) return; setBackgroundBrush(m_map->canvasColor()); for (const Room &r : m_map->rooms) { auto *item = new RoomItem(this, r.id); addItem(item); m_roomItems.insert(r.id, item); } rebuildConnections(); // Give comfortable, balanced scrolling room around the content in BOTH // axes. A map that is much wider than it is tall (or vice versa) would // otherwise get almost no vertical (resp. horizontal) scroll range once // fitted to the window: the scrollbar thumb then fills its whole track and // the smallest drag snaps from one end to the other. Sizing the margin from // the larger content dimension keeps the scroll range proportionate in both // directions, so the thumb stays a sensible size and drags smoothly. const QRectF content = itemsBoundingRect(); double margin = std::max(600.0, std::max(content.width(), content.height())); // "Infinite" scroll bounds let the view roam far past the content, rather // than clamping close to it (the C# InfiniteScrollBounds app setting). if (AppSettings::instance().infiniteScrollBounds) margin = std::max(margin, 100000.0); QRectF bounds = content.adjusted(-margin, -margin, margin, margin); if (bounds.width() < 1200 || bounds.height() < 900) bounds = bounds.united(QRectF(bounds.center() - QPointF(600, 450), QSizeF(1200, 900))); setSceneRect(bounds); emitSelectionSummary(); } void MapScene::rebuildConnections() { const QList old = m_connItems; m_connItems.clear(); for (ConnectionItem *item : old) { removeItem(item); delete item; } if (!m_map) return; for (const Connection &c : m_map->connections) { auto *item = new ConnectionItem(this, c.id); addItem(item); m_connItems.append(item); } } void MapScene::refreshConnectionsFor(int roomId) { if (!m_map) return; for (ConnectionItem *item : m_connItems) { const int idx = m_map->connectionIndex(item->connId()); if (idx < 0) continue; bool touches = false; for (const Vertex &v : m_map->connections.at(idx).vertices) { if (v.docked && v.roomId == roomId) { touches = true; break; } } if (touches) item->updateRoute(); } } void MapScene::setValidation(const ValidationFlags &flags) { m_validation = flags; update(); // repaint so the red-X overlays appear/disappear } void MapScene::refreshValidationCache() const { m_nameCounts.clear(); m_roomsWithDanglingConnections.clear(); if (!m_map) return; for (const Room &r : m_map->rooms) ++m_nameCounts[r.name]; for (const Connection &c : m_map->connections) { bool hasDangling = false; for (const Vertex &v : c.vertices) if (!v.docked) hasDangling = true; if (!hasDangling) continue; for (const Vertex &v : c.vertices) if (v.docked) m_roomsWithDanglingConnections.insert(v.roomId); } m_validationCacheDirty = false; } bool MapScene::roomInvalid(const Room &room) const { if (!m_map) return false; if (m_validation.description && room.description.trimmed().isEmpty()) return true; if (m_validation.subtitle && room.subtitle.trimmed().isEmpty()) return true; if (!m_validation.uniqueNames && !m_validation.noDangling) return false; if (m_validationCacheDirty) refreshValidationCache(); if (m_validation.uniqueNames && m_nameCounts.value(room.name) > 1) return true; if (m_validation.noDangling && m_roomsWithDanglingConnections.contains(room.id)) return true; return false; } void MapScene::refreshRoom(int roomId) { m_validationCacheDirty = true; if (RoomItem *item = m_roomItems.value(roomId, nullptr)) item->syncFromModel(); refreshConnectionsFor(roomId); } void MapScene::refreshConnection(int connId) { m_validationCacheDirty = true; for (ConnectionItem *item : m_connItems) { if (item->connId() == connId) { item->updateRoute(); return; } } } void MapScene::roomMovedTo(int roomId, const QPointF &topLeft) { if (!m_map) return; Room *room = m_map->roomById(roomId); if (!room) return; if (room->x == topLeft.x() && room->y == topLeft.y()) return; room->x = topLeft.x(); room->y = topLeft.y(); refreshConnectionsFor(roomId); } void MapScene::selectRoomItem(int roomId) { clearSelection(); if (RoomItem *item = m_roomItems.value(roomId, nullptr)) item->setSelected(true); } int MapScene::selectedRoomId() const { for (QGraphicsItem *item : selectedItems()) { if (auto *ri = dynamic_cast(item)) return ri->roomId(); } return -1; } QList MapScene::selectedRoomIds() const { QList ids; for (QGraphicsItem *item : selectedItems()) { if (auto *ri = dynamic_cast(item)) ids.append(ri->roomId()); } return ids; } QList MapScene::selectedConnectionIds() const { QList ids; for (QGraphicsItem *item : selectedItems()) { if (auto *ci = dynamic_cast(item)) ids.append(ci->connId()); } return ids; } void MapScene::selectAll() { for (QGraphicsItem *item : items()) item->setSelected(true); } void MapScene::selectRoomsByIds(const QList &ids) { clearSelection(); for (int id : ids) { if (RoomItem *item = m_roomItems.value(id, nullptr)) item->setSelected(true); } } void MapScene::selectConnectionsByIds(const QList &ids) { clearSelection(); for (ConnectionItem *item : m_connItems) { if (ids.contains(item->connId())) item->setSelected(true); } } int MapScene::addRoomAt(const QPointF &scenePos) { if (!m_map) return -1; const QPointF tl = snap(scenePos); if (m_undo) { auto *cmd = new AddRoomCommand(this, tl.x(), tl.y()); const int id = cmd->roomId(); m_undo->push(cmd); return id; } // Direct path for headless callers with no undo stack. const int id = m_map->addRoom(tl.x(), tl.y()); auto *item = new RoomItem(this, id); addItem(item); m_roomItems.insert(id, item); m_validationCacheDirty = true; setSceneRect(sceneRect().united(item->sceneBoundingRect().adjusted(-400, -400, 400, 400))); return id; } void MapScene::deleteSelection() { if (!m_map) return; QList roomsToDelete; QList connsToDelete; for (QGraphicsItem *item : selectedItems()) { if (auto *ri = dynamic_cast(item)) roomsToDelete.append(ri->roomId()); else if (auto *ci = dynamic_cast(item)) connsToDelete.append(ci->connId()); } if (roomsToDelete.isEmpty() && connsToDelete.isEmpty()) return; if (m_undo) { auto *cmd = new DeleteElementsCommand(this, roomsToDelete, connsToDelete); if (cmd->isEmpty()) delete cmd; else m_undo->push(cmd); return; } for (int id : connsToDelete) m_map->removeConnection(id); for (int id : roomsToDelete) m_map->removeRoom(id); // also drops connections docked to it rebuild(); } void MapScene::deleteSelectionLater() { QTimer::singleShot(0, this, &MapScene::deleteSelection); } void MapScene::addToSelection(const QList &roomIds, const QList &connIds) { for (int id : roomIds) { if (RoomItem *item = m_roomItems.value(id, nullptr)) item->setSelected(true); } if (connIds.isEmpty()) return; for (ConnectionItem *item : m_connItems) { if (connIds.contains(item->connId())) item->setSelected(true); } } void MapScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if (itemAt(event->scenePos(), QTransform())) { QGraphicsScene::contextMenuEvent(event); return; } emit canvasContextMenuRequested(event->scenePos(), event->screenPos()); event->accept(); } void MapScene::setConnectMode(bool on) { m_connectMode = on; // In connect mode, rooms should not drag; toggle their movability. for (RoomItem *item : m_roomItems) item->setFlag(QGraphicsItem::ItemIsMovable, !on); } RoomItem *MapScene::roomItemAt(const QPointF &scenePos) const { const QList hit = items(scenePos); for (QGraphicsItem *item : hit) { if (auto *ri = dynamic_cast(item)) return ri; } return nullptr; } void MapScene::activateRoom(int roomId) { emit editRoomRequested(roomId); } void MapScene::activateConnection(int connId) { emit editConnectionRequested(connId); } void MapScene::emitSelectionSummary() { if (!m_map) { emit selectionSummary(QString()); return; } emit selectionSummary(tr("%1 rooms, %2 connections") .arg(m_map->rooms.size()) .arg(m_map->connections.size())); } void MapScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (m_connectMode && event->button() == Qt::LeftButton) { if (RoomItem *from = roomItemAt(event->scenePos())) { m_connectFromRoom = from->roomId(); // Remember which compass port the drag started nearest to, so the // connection docks there rather than at an auto-computed port. m_connectFromPort.clear(); if (m_map) { if (const Room *fr = m_map->roomById(from->roomId())) m_connectFromPort = nearestPort(*fr, event->scenePos(), AppSettings::instance().portAdjustDetail); } m_rubberLine = new QGraphicsLineItem(QLineF(event->scenePos(), event->scenePos())); QPen pen(QColor(30, 120, 220)); pen.setWidthF(2.0); pen.setStyle(Qt::DashLine); m_rubberLine->setPen(pen); m_rubberLine->setZValue(10); addItem(m_rubberLine); event->accept(); return; } } QGraphicsScene::mousePressEvent(event); // Snapshot the positions of the rooms about to be dragged, so the move can // be pushed as one undoable command on release. m_dragStartPos.clear(); if (event->button() == Qt::LeftButton && !m_connectMode && !m_roomResizeActive && m_map) { for (QGraphicsItem *item : selectedItems()) { if (auto *ri = dynamic_cast(item)) { if (const Room *r = m_map->roomById(ri->roomId())) m_dragStartPos.insert(ri->roomId(), QPointF(r->x, r->y)); } } } } void MapScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_rubberLine) { const QLineF line = m_rubberLine->line(); m_rubberLine->setLine(QLineF(line.p1(), event->scenePos())); event->accept(); return; } QGraphicsScene::mouseMoveEvent(event); } void MapScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (m_rubberLine) { removeItem(m_rubberLine); delete m_rubberLine; m_rubberLine = nullptr; const QPointF downPx = event->buttonDownScreenPos(Qt::LeftButton); const QPointF upPx = event->screenPos(); const bool draggedFarEnough = std::hypot(upPx.x() - downPx.x(), upPx.y() - downPx.y()) >= kDragToConnectPixels; RoomItem *target = draggedFarEnough ? roomItemAt(event->scenePos()) : nullptr; if (m_map && target) { const Room *from = m_map->roomById(m_connectFromRoom); const Room *to = m_map->roomById(target->roomId()); if (from && to) { QString portA; QString portB; if (from->id == to->id) { // Self-loop: two adjacent top ports so the stalks bulge out. portA = QStringLiteral("nw"); portB = QStringLiteral("ne"); } else { // Dock the source at the port the drag began from and the // target at the port nearest where it was released, so the // user chooses both ends; fall back to the facing port. const int detail = AppSettings::instance().portAdjustDetail; portA = m_connectFromPort.isEmpty() ? Map::portFacing(*from, *to) : m_connectFromPort; portB = nearestPort(*to, event->scenePos(), detail); } if (m_undo) { m_undo->push( new AddConnectionCommand(this, from->id, portA, to->id, portB)); } else { m_map->addConnection(from->id, portA, to->id, portB); rebuildConnections(); m_validationCacheDirty = true; } } } m_connectFromRoom = -1; event->accept(); return; } QGraphicsScene::mouseReleaseEvent(event); // If a drag moved one or more rooms, record it as a single undoable command. if (!m_dragStartPos.isEmpty()) { QList moves; for (auto it = m_dragStartPos.constBegin(); it != m_dragStartPos.constEnd(); ++it) { if (const Room *r = m_map ? m_map->roomById(it.key()) : nullptr) { const QPointF cur(r->x, r->y); if (cur != it.value()) moves.append({it.key(), it.value(), cur}); } } m_dragStartPos.clear(); if (!moves.isEmpty() && m_undo) m_undo->push(new MoveRoomsCommand(this, moves)); } } void MapScene::drawBackground(QPainter *painter, const QRectF &rect) { QGraphicsScene::drawBackground(painter, rect); if (!m_map || !m_map->settings.gridVisible) return; const double g = gridSize(); if (g < 2.0) return; QColor gridColor = m_map->settings.colors[ColorGrid]; if (!gridColor.isValid()) gridColor = QColor(235, 235, 235); QPen pen(gridColor); pen.setWidthF(0.0); // cosmetic painter->setPen(pen); const double left = std::floor(rect.left() / g) * g; const double top = std::floor(rect.top() / g) * g; for (double x = left; x < rect.right(); x += g) painter->drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom())); for (double y = top; y < rect.bottom(); y += g) painter->drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)); if (m_map->settings.showOrigin) { // A small "+" spanning one grid step each way from the origin (as the C# // Canvas draws it), tinted canvas:smallText 3:1, not a full crosshair. QColor canvas = m_map->canvasColor(); if (!canvas.isValid()) canvas = QColor(Qt::white); QColor small = m_map->settings.colors[ColorSmallText]; if (!small.isValid()) small = QColor(60, 60, 60); const QColor originColor((3 * canvas.red() + small.red()) / 4, (3 * canvas.green() + small.green()) / 4, (3 * canvas.blue() + small.blue()) / 4); QPen originPen(originColor); originPen.setWidthF(0.0); painter->setPen(originPen); painter->drawLine(QPointF(-g, 0), QPointF(g, 0)); painter->drawLine(QPointF(0, -g), QPointF(0, g)); } } } // namespace trizbort trizbort-qt-1.0.1/src/MapScene.h000066400000000000000000000232031526521612000164450ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAPSCENE_H #define TRIZBORT_MAPSCENE_H #include #include #include #include #include #include #include #include #include "MapDocument.h" class QUndoStack; namespace trizbort { class RoomItem; class ConnectionItem; // The editing canvas: an interactive QGraphicsScene backed by a live Map. Rooms // and connections are individual items; dragging a room writes back to the // model and reroutes its connections. A connect tool draws new connections by // dragging between rooms. class MapScene : public QGraphicsScene { Q_OBJECT public: explicit MapScene(QObject *parent = nullptr); ~MapScene() override; // Bind to a document (not owned) and (re)build all items. void setDocument(Map *map); Map *document() const { return m_map; } void rebuild(); // The undo stack edits are pushed to (not owned). When unset, edits apply // directly (used by headless callers and rendering). void setUndoStack(QUndoStack *stack) { m_undo = stack; } QUndoStack *undoStack() const { return m_undo; } // Select only the given room's item (used after an add). void selectRoomItem(int roomId); // The id of a currently-selected room (the first one), or -1. int selectedRoomId() const; // The ids of all currently-selected rooms / connections. QList selectedRoomIds() const; QList selectedConnectionIds() const; // Select every room and connection. void selectAll(); // Replace the selection with the given rooms / connections by id. void selectRoomsByIds(const QList &ids); void selectConnectionsByIds(const QList &ids); double gridSize() const; QPointF snap(const QPointF &p) const; // The world-space point of a room's port. Supports all sixteen compass // ports (n, nne, ne, ene, e, ...) mapped onto the room's outline, and is // shape-aware for ellipse and octagonal rooms; center for an unknown token. static QPointF portPoint(const Room &room, const QString &port); // The outward "stalk" point a docked connection endpoint runs to before it // reaches the room outline, mirroring Room.GetPortStalkPosition. Returns the // port point itself when stalk <= 0 (no stalk). static QPointF portStalkPoint(const Room &room, const QString &port, double stalk); // A room's port point on its plain bounding rectangle (no shape awareness). static QPointF squareCorner(double x, double y, double w, double h, const QString &port); // The 8-way compass port on a room that best faces a scene point. static QString portTowards(const Room &room, const QPointF &target); // The compass port tokens offered at a given granularity (the app's // PortAdjustDetail): 4 (NSEW), 8 (+ diagonals) or 16 (+ half-winds). static QStringList portTokensForDetail(int detail); // The port token (at that granularity) whose point is nearest a scene point. static QString nearestPort(const Room &room, const QPointF &scenePoint, int detail); // The id of the room whose outline contains a scene point, or -1. int roomIdAt(const QPointF &scenePos) const; // The id of the room nearest a scene point whose rectangle is within maxDist // of it (0 => must contain the point), or -1. Implements snap-to-element // magnetism when re-docking connection endpoints. int roomNearestWithin(const QPointF &scenePos, double maxDist) const; QVector> connectionSegmentsNear(const ConnectionItem *item) const; // Editing entry points used by the window's actions. int addRoomAt(const QPointF &scenePos); // returns new room id void deleteSelection(); void deleteSelectionLater(); void addToSelection(const QList &roomIds, const QList &connIds); void setConnectMode(bool on); bool connectMode() const { return m_connectMode; } // Default style/flow applied to connections drawn in connect mode. void setNewConnectionStyle(ConnectionStyle style) { m_newConnStyle = style; } void setNewConnectionFlow(ConnectionFlow flow) { m_newConnFlow = flow; } ConnectionStyle newConnectionStyle() const { return m_newConnStyle; } ConnectionFlow newConnectionFlow() const { return m_newConnFlow; } // Called by RoomItem while dragging: persist the new position and reroute. void roomMovedTo(int roomId, const QPointF &topLeft); // While a room is being resized by its handle, the scene must not treat the // drag as a move (which would push a spurious MoveRoomsCommand). void setRoomResizeActive(bool active) { m_roomResizeActive = active; } // Re-route the connections docked to a room (used during a resize drag). void refreshConnectionsFor(int roomId); // Called by items on double-click. void activateRoom(int roomId); void activateConnection(int connId); // Room validation. These flags are runtime-only (not persisted), mirroring // the C# project validation toggles; invalid rooms get a red X overlay. struct ValidationFlags { bool uniqueNames = false; bool description = false; bool subtitle = false; bool noDangling = false; }; void setValidation(const ValidationFlags &flags); ValidationFlags validation() const { return m_validation; } bool roomInvalid(const Room &room) const; // Toggle drawing of room/connection text (View > Toggle Text). void setTextVisible(bool visible) { m_textVisible = visible; update(); } bool textVisible() const { return m_textVisible; } // Refresh a single room's visuals and the connections touching it. void refreshRoom(int roomId); // Re-route and repaint one connection after its properties changed. void refreshConnection(int connId); signals: void editRoomRequested(int roomId); // open the room properties dialog void editConnectionRequested(int connId); void selectionSummary(const QString &text); void canvasContextMenuRequested(const QPointF &scenePos, const QPoint &screenPos); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; void drawBackground(QPainter *painter, const QRectF &rect) override; private: void rebuildConnections(); RoomItem *roomItemAt(const QPointF &scenePos) const; void emitSelectionSummary(); void refreshValidationCache() const; Map *m_map = nullptr; QHash m_roomItems; QList m_connItems; mutable bool m_validationCacheDirty = true; mutable QHash m_nameCounts; mutable QSet m_roomsWithDanglingConnections; bool m_clearing = false; bool m_connectMode = false; bool m_roomResizeActive = false; bool m_textVisible = true; ConnectionStyle m_newConnStyle = ConnectionStyle::Solid; ConnectionFlow m_newConnFlow = ConnectionFlow::TwoWay; ValidationFlags m_validation; int m_connectFromRoom = -1; QString m_connectFromPort; // port picked when the drag started QGraphicsLineItem *m_rubberLine = nullptr; QUndoStack *m_undo = nullptr; QHash m_dragStartPos; // room id -> position at drag start }; } // namespace trizbort #endif // TRIZBORT_MAPSCENE_H trizbort-qt-1.0.1/src/MapStatisticsDialog.h000066400000000000000000000540031526521612000206640ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAPSTATISTICSDIALOG_H #define TRIZBORT_MAPSTATISTICSDIALOG_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MapDocument.h" namespace trizbort { // Build a human-readable statistics report for a map, mirroring the figures and // wording of the C# MapStatisticsView so the two read identically. inline QString buildStatisticsReport(const Map &map) { auto plur = [](int x) { return x == 1 ? QString() : QStringLiteral("s"); }; auto objectCount = [](const Room &r) -> int { QString t = r.objectsText; t.remove(QRegularExpression(QStringLiteral("\\[[^\\]\\[]*\\]"))); int n = 0; for (const QString &line : t.split(QLatin1Char('\n'))) if (!line.trimmed().isEmpty()) ++n; return n; }; // A connection's endpoints in index order. auto sortedVerts = [](const Connection &c) { QList v = c.vertices; std::sort(v.begin(), v.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); return v; }; // Source/target room ids (−1 when that end isn't docked to a room). auto sourceRoom = [&](const Connection &c) -> int { const QList v = sortedVerts(c); return (!v.isEmpty() && v.first().docked) ? v.first().roomId : -1; }; auto targetRoom = [&](const Connection &c) -> int { const QList v = sortedVerts(c); return (v.size() > 1 && v.last().docked) ? v.last().roomId : -1; }; // Compass port index 0..15 (n, nne, ne, …), or −1 for a non-compass end. auto compassIndex = [](const QString &port) -> int { static const char *const names[16] = {"n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"}; const QString p = port.toLower(); for (int i = 0; i < 16; ++i) if (p == QLatin1String(names[i])) return i; return -1; }; auto firstPortIndex = [&](const Connection &c) -> int { const QList v = sortedVerts(c); return (!v.isEmpty() && v.first().docked) ? compassIndex(v.first().port) : -1; }; auto secondPortIndex = [&](const Connection &c) -> int { const QList v = sortedVerts(c); return (v.size() > 1 && v.at(1).docked) ? compassIndex(v.at(1).port) : -1; }; auto opposite = [](int idx) { return idx < 0 ? -1 : (idx + 8) % 16; }; // Two compass directions in the same cardinal group (diagonals match only // themselves), matching Canvas.EqualEnough. auto equalEnough = [](int a, int b) -> bool { if (a < 0 || b < 0) return false; if (a == b) return true; auto group = [](int i) -> int { switch (i) { case 0: case 1: case 15: return 0; // N, NNE, NNW case 4: case 3: case 5: return 1; // E, ENE, ESE case 8: case 7: case 9: return 2; // S, SSE, SSW case 12: case 13: case 11: return 3; // W, WNW, WSW default: return -1; // NE, SE, SW, NW } }; const int ga = group(a); return ga >= 0 && ga == group(b); }; const qsizetype rooms = map.rooms.size(); const qsizetype connections = map.connections.size(); // Rooms docked to each connection, and the connections touching each room. QHash> roomConnections; // roomId -> connection indices for (const Room &r : map.rooms) roomConnections.insert(r.id, {}); int dangling = 0, selfLoops = 0, oneWay = 0, dashed = 0; int doors = 0, lockedDoors = 0, lockableDoors = 0, openableDoors = 0, openDoors = 0; int upDown = 0, inOut = 0, labeled = 0, middleText = 0; int diagonal2 = 0, diagonal1 = 0, bentAll = 0, bentNoText = 0; for (int ci = 0; ci < map.connections.size(); ++ci) { const Connection &c = map.connections.at(ci); if (c.flow == ConnectionFlow::OneWay) ++oneWay; if (c.style == ConnectionStyle::Dashed) ++dashed; if (c.hasDoor) { ++doors; if (c.door.locked) ++lockedDoors; if (c.door.lockable) ++lockableDoors; if (c.door.openable) ++openableDoors; if (c.door.open) ++openDoors; } const bool hasStart = !c.startText.isEmpty(); const bool hasEnd = !c.endText.isEmpty(); if (hasStart || hasEnd) ++labeled; if (!c.midText.isEmpty()) ++middleText; if ((c.endText == QLatin1String("up") && c.startText == QLatin1String("down")) || (c.endText == QLatin1String("down") && c.startText == QLatin1String("up"))) ++upDown; if ((c.endText == QLatin1String("in") && c.startText == QLatin1String("out")) || (c.endText == QLatin1String("out") && c.startText == QLatin1String("in"))) ++inOut; const int p1 = firstPortIndex(c); const int p2 = secondPortIndex(c); int diags = 0; if (p1 >= 0 && (p1 % 4) == 2 && !hasStart) ++diags; if (p2 >= 0 && (p2 % 4) == 2 && !hasEnd) ++diags; if (diags == 2) ++diagonal2; else if (diags == 1) ++diagonal1; if (p1 >= 0 && p2 >= 0 && !equalEnough(p1, opposite(p2))) { ++bentAll; if (!hasStart && !hasEnd) ++bentNoText; } const int src = sourceRoom(c); const int tgt = targetRoom(c); if (src < 0 || tgt < 0) ++dangling; if (src >= 0 && src == tgt) ++selfLoops; if (roomConnections.contains(src)) roomConnections[src].append(ci); if (tgt != src && roomConnections.contains(tgt)) roomConnections[tgt].append(ci); } // Custom connections: labeled ones that aren't the up/down or in/out pairs. const int customConnections = labeled - inOut - upDown; int dark = 0, described = 0, subtitled = 0, withObjects = 0, totalObjects = 0; int obj1 = 0, obj2 = 0, obj3plus = 0; int rect = 0, rounded = 0, ellipse = 0, octagon = 0; int startRoomCount = 0, endRoomCount = 0; QString startRoomName; QStringList endNames; for (const Room &r : map.rooms) { if (r.isDark) ++dark; if (!r.description.trimmed().isEmpty()) ++described; if (!r.subtitle.trimmed().isEmpty()) ++subtitled; const int oc = objectCount(r); if (oc > 0) ++withObjects; if (oc == 1) ++obj1; else if (oc == 2) ++obj2; else if (oc >= 3) ++obj3plus; totalObjects += oc; if (r.ellipse) ++ellipse; else if (r.octagonal) ++octagon; else if (r.roundedCorners) ++rounded; else ++rect; if (r.isStartRoom) { ++startRoomCount; if (startRoomName.isEmpty()) startRoomName = r.name.isEmpty() ? QObject::tr("(unnamed)") : r.name; } if (r.isEndRoom) { ++endRoomCount; endNames << (r.name.isEmpty() ? QObject::tr("(unnamed)") : r.name); } } // Dead ends and unconnected (floating) rooms, matching the C# definitions. int deadEnds = 0, floating = 0; if (rooms == 1) { deadEnds = 1; // the single room is where you start, so it's a dead end } for (const Room &r : map.rooms) { const QVector &conns = roomConnections.value(r.id); // Unconnected: no connections, or every non-dangling one loops on itself. bool allLoops = true; for (int ci : conns) { const Connection &c = map.connections.at(ci); const int src = sourceRoom(c); const int tgt = targetRoom(c); if (src >= 0 && tgt >= 0 && src != tgt) allLoops = false; } if (conns.isEmpty() || allLoops) ++floating; if (rooms != 1) { if (conns.isEmpty()) { if (r.isStartRoom) ++deadEnds; } else { bool isDead = true; int waysIn = 0; for (int ci : conns) { const Connection &c = map.connections.at(ci); const int src = sourceRoom(c); const int tgt = targetRoom(c); if (src < 0 || tgt < 0) continue; // dangling: no way in if (c.flow == ConnectionFlow::TwoWay) { if (src != tgt) { isDead = false; break; } } else if (src == r.id) { isDead = false; // room starts a one-way exit break; } else { ++waysIn; } } if (isDead && waysIn > 0) ++deadEnds; } } } // Duplicate room names: "Name(count), …" or "None". QHash nameCounts; for (const Room &r : map.rooms) nameCounts[r.name.toLower()] += 1; QStringList dupes; QSet dupeSeen; for (const Room &r : map.rooms) { const QString key = r.name.toLower(); if (nameCounts.value(key) > 1 && !dupeSeen.contains(key)) { dupeSeen.insert(key); dupes << QStringLiteral("%1(%2)").arg(r.name).arg(nameCounts.value(key)); } } // Rooms with a duplicated exit label in a given direction word. auto dupConnectionList = [&](const QString &word) -> QString { QStringList names; QList sorted = map.rooms; std::sort(sorted.begin(), sorted.end(), [](const Room &a, const Room &b) { return a.name < b.name; }); for (const Room &r : sorted) { int dupCount = 0; for (int ci : roomConnections.value(r.id)) { const Connection &c = map.connections.at(ci); if (targetRoom(c) == r.id && c.endText == word) ++dupCount; if (sourceRoom(c) == r.id && c.startText == word) ++dupCount; } if (dupCount > 1) names << r.name; } if (names.isEmpty()) return QObject::tr("No rooms with duplicate %1 exits.").arg(word); return QObject::tr("Rooms with duplicate %1 exits: %2.").arg(word, names.join(QStringLiteral(", "))); }; // Element bounds (rooms and free connection vertices), with and without the // page margins. double minX = std::numeric_limits::max(), minY = minX; double maxX = std::numeric_limits::lowest(), maxY = maxX; auto expand = [&](double x, double y) { minX = qMin(minX, x); minY = qMin(minY, y); maxX = qMax(maxX, x); maxY = qMax(maxY, y); }; for (const Room &r : map.rooms) { expand(r.x, r.y); expand(r.x + r.w, r.y + r.h); } for (const Connection &c : map.connections) for (const Vertex &v : c.vertices) if (!v.docked) expand(v.point.x(), v.point.y()); const double rawW = maxX - minX; const double rawH = maxY - minY; double hMargin = 0.0, vMargin = 0.0; if (map.settings.documentSpecificMargins) { hMargin = map.settings.horizontalMargin; vMargin = map.settings.verticalMargin; } else { // The C# font-height fudge: max(subtitle height, 4x object height). const double subH = (map.settings.subtitleFont.size > 0 ? map.settings.subtitleFont.size : 11.0) * 1.4; const double objH = (map.settings.objectFont.size > 0 ? map.settings.objectFont.size : 11.0) * 1.4; hMargin = vMargin = qMax(subH, objH * 4.0); } QString out; QTextStream s(&out); if (rooms == 0) { s << QObject::tr("No rooms to take stats of."); return out; } s << QObject::tr("# of Rooms: %1").arg(rooms) << "\n"; s << QObject::tr("# of Dark Rooms: %1").arg(dark) << "\n"; s << QObject::tr("# of Unconnected Rooms: %1").arg(floating) << "\n\n"; if (startRoomCount == 1) s << QObject::tr("Start room = %1").arg(startRoomName); else if (startRoomCount > 1) s << QObject::tr("More than one start room."); else s << QObject::tr("No start room."); s << "\n"; if (endRoomCount == 1) s << QObject::tr("End room = %1").arg(endNames.first()); else if (endRoomCount > 1) s << QObject::tr("End rooms = (%1)").arg(endNames.join(QStringLiteral(", "))); else s << QObject::tr("No end room."); s << "\n"; s << "\n" << QObject::tr("Dimensions with margins: height %1, width %2") .arg(qRound(rawH + 2 * vMargin)) .arg(qRound(rawW + 2 * hMargin)) << "\n"; s << QObject::tr("Dimensions without margins: height %1, width %2") .arg(qRound(rawH)) .arg(qRound(rawW)) << "\n\n"; s << QObject::tr("# of Connections: %1 total, %2 unlabeled, %3 one-way, %4 dashed/dotted, " "%5 up/down, %6 in/out.") .arg(connections) .arg(labeled) .arg(oneWay) .arg(dashed) .arg(upDown) .arg(inOut) << "\n"; s << QObject::tr("# of Dangling Connections: %1").arg(dangling) << "\n"; s << QObject::tr("# of Self Looping Connections: %1").arg(selfLoops) << "\n"; s << QObject::tr("# of Dead Ends: %1").arg(deadEnds) << "\n\n"; s << QObject::tr("%1 door%2, %3 locked, %4 lockable, %5 open, %6 openable") .arg(doors) .arg(plur(doors)) .arg(lockedDoors) .arg(lockableDoors) .arg(openDoors) .arg(openableDoors) << "\n\n"; // Regions. int namedRegions = 0; int withoutRegion = 0; for (const Room &r : map.rooms) if (r.region.isEmpty() || r.region == kNoRegion) ++withoutRegion; QList sortedRegions; for (const Region &rg : map.regions) if (rg.name != kNoRegion && !rg.name.isEmpty()) { sortedRegions << rg; ++namedRegions; } std::sort(sortedRegions.begin(), sortedRegions.end(), [](const Region &a, const Region &b) { return a.name < b.name; }); auto roomsInRegion = [&](const QString ®ion) { int n = 0; for (const Room &r : map.rooms) if (r.region == region) ++n; return n; }; s << QObject::tr("# of Regions: %1").arg(namedRegions) << "\n"; s << QObject::tr("# of Rooms without a region: %1").arg(withoutRegion) << "\n"; if (namedRegions > 0) { s << QObject::tr("Regions:") << "\n"; for (const Region &rg : sortedRegions) { const int n = roomsInRegion(rg.name); s << QStringLiteral("%1 (%2 %3)").arg(rg.name).arg(n).arg(n == 1 ? QObject::tr("room") : QObject::tr("rooms")) << "\n"; } } s << "\n"; s << QObject::tr("Total # Objects in All Rooms: %1").arg(totalObjects) << "\n"; s << QObject::tr("Total # Rooms with Objects: %1, %2 with 1, %3 with 2, %4 with 3+") .arg(withObjects) .arg(obj1) .arg(obj2) .arg(obj3plus) << "\n\n"; s << QObject::tr("Total # of Rooms with subtitles: %1").arg(subtitled) << "\n"; s << QObject::tr("Total # of Rooms with descriptions: %1").arg(described) << "\n"; s << "\n" << QString(30, QLatin1Char('=')) << QObject::tr("Odd stuff below here") << "\n"; s << "\n" << QObject::tr("Duplicate rooms: %1.") .arg(dupes.isEmpty() ? QObject::tr("None") : dupes.join(QStringLiteral(", "))) << "\n"; s << "\n" << QObject::tr("Room shapes: %1 rectangular, %2 elliptical, %3 round cornered, %4 octagonal.") .arg(rect) .arg(ellipse) .arg(rounded) .arg(octagon) << "\n"; if (namedRegions > 0) { s << "\n"; for (const Region &rg : sortedRegions) { const int n = roomsInRegion(rg.name); if (n == 0) s << QObject::tr("%1 has no rooms.").arg(rg.name); else if (n == 1) s << QObject::tr("Only room in %1: ").arg(rg.name); else s << QObject::tr("List of rooms in %1: ").arg(rg.name); QStringList names; QList sortedRooms = map.rooms; std::sort(sortedRooms.begin(), sortedRooms.end(), [](const Room &a, const Room &b) { return a.name < b.name; }); for (const Room &r : sortedRooms) if (r.region == rg.name) names << r.name; s << names.join(QStringLiteral(", ")) << "\n"; } s << "\n"; } else { s << "\n"; } s << QObject::tr("%1 up-down connection%2").arg(upDown).arg(plur(upDown)) << "\n"; s << QObject::tr("%1 in-out connection%2").arg(inOut).arg(plur(inOut)) << "\n"; s << QObject::tr("%1 custom connection%2").arg(customConnections).arg(plur(customConnections)) << "\n"; s << QObject::tr("%1 diagonal connection%2 (2-way)").arg(diagonal2).arg(plur(diagonal2)) << "\n"; s << QObject::tr("%1 diagonal connection%2 (1-way)").arg(diagonal1).arg(plur(diagonal1)) << "\n"; s << QObject::tr("%1 connection%2 with middle text").arg(middleText).arg(plur(middleText)) << "\n"; s << QObject::tr("%1 bent connection%2, %3 with no text").arg(bentAll).arg(plur(bentAll)).arg(bentNoText) << "\n\n"; s << dupConnectionList(QStringLiteral("in")) << "\n"; s << dupConnectionList(QStringLiteral("out")) << "\n"; s << dupConnectionList(QStringLiteral("up")) << "\n"; s << dupConnectionList(QStringLiteral("down")) << "\n"; return out; } // A read-only view of the statistics report with an Export-to-file button. class MapStatisticsDialog : public QDialog { public: MapStatisticsDialog(const Map &map, QWidget *parent = nullptr) : QDialog(parent) , m_report(buildStatisticsReport(map)) { setWindowTitle(tr("Map Statistics")); resize(520, 480); auto *layout = new QVBoxLayout(this); auto *text = new QPlainTextEdit(this); text->setReadOnly(true); text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); text->setPlainText(m_report); layout->addWidget(text); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this); auto *exportButton = buttons->addButton(tr("&Export…"), QDialogButtonBox::ActionRole); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::accept); connect(exportButton, &QPushButton::clicked, this, [this] { exportReport(); }); layout->addWidget(buttons); } private: void exportReport() { const QString path = QFileDialog::getSaveFileName( this, tr("Export Statistics"), QStringLiteral("statistics.log"), tr("Log files (*.log);;Text files (*.txt);;All files (*)")); if (path.isEmpty()) return; QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Export Failed"), file.errorString()); return; } QTextStream(&file) << m_report; } QString m_report; }; } // namespace trizbort #endif // TRIZBORT_MAPSTATISTICSDIALOG_H trizbort-qt-1.0.1/src/MapView.cpp000066400000000000000000000154011526521612000166560ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "MapView.h" #include #include #include #include #include #include #include #include #include "MapScene.h" namespace trizbort { MapView::MapView(QWidget *parent) : QGraphicsView(parent) { setRenderHint(QPainter::Antialiasing, true); setRenderHint(QPainter::TextAntialiasing, true); setDragMode(QGraphicsView::RubberBandDrag); setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setResizeAnchor(QGraphicsView::AnchorViewCenter); #ifdef Q_OS_MACOS setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); #endif setViewportUpdateMode(QGraphicsView::FullViewportUpdate); } void MapView::scaleClamped(double factor) { const double current = transform().m11(); if (current <= 0.0 || factor <= 0.0) return; const double target = std::clamp(current * factor, kMinZoom, kMaxZoom); const double applied = target / current; if (std::abs(applied - 1.0) < 1e-9) return; scale(applied, applied); } void MapView::panByPixels(const QPoint &delta) { horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); } void MapView::zoomIn() { scaleClamped(1.15); } void MapView::zoomOut() { scaleClamped(1.0 / 1.15); } void MapView::resetZoom() { resetTransform(); } void MapView::setZoomPercent(double percent) { resetTransform(); const double f = std::clamp(percent / 100.0, kMinZoom, kMaxZoom); scale(f, f); } void MapView::microZoom(bool in) { scaleClamped(in ? 1.01 : 1.0 / 1.01); } void MapView::resetOrigin() { resetTransform(); centerOn(0, 0); } void MapView::zoomToFit() { if (!scene()) return; fitInView(scene()->itemsBoundingRect().adjusted(-40, -40, 40, 40), Qt::KeepAspectRatio); const double f = transform().m11(); const double clamped = std::clamp(f, kMinZoom, kMaxZoom); if (f > 0.0 && std::abs(clamped - f) > 1e-9) scaleClamped(clamped / f); } void MapView::wheelEvent(QWheelEvent *event) { if (!event->pixelDelta().isNull() && !(event->modifiers() & Qt::ControlModifier)) { panByPixels(event->pixelDelta()); event->accept(); return; } double delta = event->angleDelta().y(); if (delta == 0.0) delta = event->angleDelta().x(); if (delta == 0.0) { event->accept(); return; } double steps = delta / 120.0; if (m_invertWheel) steps = -steps; const double perNotch = (event->modifiers() & Qt::ControlModifier) ? 1.05 : 1.15; scaleClamped(std::pow(perNotch, steps)); event->accept(); } void MapView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::MiddleButton) { m_panning = true; m_lastPanPoint = event->pos(); setCursor(Qt::ClosedHandCursor); event->accept(); return; } m_additiveSelect = false; auto *mapScene = qobject_cast(scene()); if (event->button() == Qt::LeftButton && (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)) && dragMode() == QGraphicsView::RubberBandDrag && mapScene && !mapScene->itemAt(mapToScene(event->pos()), transform())) { m_additiveSelect = true; m_savedRoomSelection = mapScene->selectedRoomIds(); m_savedConnectionSelection = mapScene->selectedConnectionIds(); } QGraphicsView::mousePressEvent(event); } void MapView::mouseMoveEvent(QMouseEvent *event) { if (m_panning) { const QPoint delta = event->pos() - m_lastPanPoint; m_lastPanPoint = event->pos(); horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); event->accept(); return; } QGraphicsView::mouseMoveEvent(event); } void MapView::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::MiddleButton && m_panning) { m_panning = false; setCursor(Qt::ArrowCursor); event->accept(); return; } QGraphicsView::mouseReleaseEvent(event); if (m_additiveSelect) { if (auto *mapScene = qobject_cast(scene())) mapScene->addToSelection(m_savedRoomSelection, m_savedConnectionSelection); m_savedRoomSelection.clear(); m_savedConnectionSelection.clear(); m_additiveSelect = false; } } } // namespace trizbort trizbort-qt-1.0.1/src/MapView.h000066400000000000000000000071121526521612000163230ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MAPVIEW_H #define TRIZBORT_MAPVIEW_H #include class QWheelEvent; class QMouseEvent; namespace trizbort { // A QGraphicsView for the editing canvas: rubber-band selection, mouse-wheel // zoom, and middle-button panning. class MapView : public QGraphicsView { public: explicit MapView(QWidget *parent = nullptr); void zoomIn(); void zoomOut(); void resetZoom(); void zoomToFit(); void setZoomPercent(double percent); // absolute zoom, 100 == 1:1 void microZoom(bool in); // +/- 1% step void resetOrigin(); // 1:1 and center the map origin void setInvertWheelZoom(bool invert) { m_invertWheel = invert; } static constexpr double kMinZoom = 0.1; static constexpr double kMaxZoom = 100.0; protected: void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private: void scaleClamped(double factor); void panByPixels(const QPoint &delta); bool m_panning = false; bool m_invertWheel = false; QPoint m_lastPanPoint; bool m_additiveSelect = false; QList m_savedRoomSelection; QList m_savedConnectionSelection; }; } // namespace trizbort #endif // TRIZBORT_MAPVIEW_H trizbort-qt-1.0.1/src/MinimapView.h000066400000000000000000000114471526521612000172060ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_MINIMAPVIEW_H #define TRIZBORT_MINIMAPVIEW_H #include #include #include #include #include #include #include "MapView.h" namespace trizbort { // A non-interactive overview of the whole scene. It shares the editing scene, // always fits the entire map, draws the main view's visible rectangle, and // recenters the main view when clicked or dragged. Header-only: it overrides // virtuals and holds a raw pointer to the main view, so it needs no moc. class MinimapView : public QGraphicsView { public: MinimapView(QGraphicsScene *scene, MapView *mainView, QWidget *parent = nullptr) : QGraphicsView(scene, parent) , m_main(mainView) { setInteractive(false); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setRenderHint(QPainter::Antialiasing, true); setFrameStyle(QFrame::NoFrame); setMinimumSize(140, 110); setCursor(Qt::PointingHandCursor); fit(); connect(scene, &QGraphicsScene::sceneRectChanged, this, [this] { fit(); }); for (QScrollBar *bar : {m_main->horizontalScrollBar(), m_main->verticalScrollBar()}) { connect(bar, &QScrollBar::valueChanged, this, [this] { viewport()->update(); }); connect(bar, &QScrollBar::rangeChanged, this, [this] { viewport()->update(); }); } } protected: void resizeEvent(QResizeEvent *event) override { QGraphicsView::resizeEvent(event); fit(); } void drawForeground(QPainter *painter, const QRectF &) override { if (!m_main || !m_main->scene()) return; const QRectF visible = m_main->mapToScene(m_main->viewport()->rect()).boundingRect(); QPen pen(QColor(30, 120, 220)); pen.setCosmetic(true); pen.setWidth(2); painter->setPen(pen); painter->setBrush(QColor(30, 120, 220, 40)); painter->drawRect(visible); } void mousePressEvent(QMouseEvent *event) override { recenter(event->pos()); } void mouseMoveEvent(QMouseEvent *event) override { if (event->buttons() & Qt::LeftButton) recenter(event->pos()); } private: void fit() { if (scene()) fitInView(scene()->sceneRect(), Qt::KeepAspectRatio); } void recenter(const QPoint &pos) { if (m_main) m_main->centerOn(mapToScene(pos)); } MapView *m_main; }; } // namespace trizbort #endif // TRIZBORT_MINIMAPVIEW_H trizbort-qt-1.0.1/src/QuickFindDialog.h000066400000000000000000000116131526521612000177510ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_QUICKFINDDIALOG_H #define TRIZBORT_QUICKFINDDIALOG_H #include #include #include #include #include #include "MapDocument.h" namespace trizbort { // Incremental find over the map: type to filter rooms by name, subtitle, // description or objects; the chosen room's id is returned via selectedRoomId(). class QuickFindDialog : public QDialog { public: explicit QuickFindDialog(const Map &map, QWidget *parent = nullptr) : QDialog(parent) , m_map(map) { setWindowTitle(tr("Find Room")); resize(360, 400); auto *layout = new QVBoxLayout(this); m_query = new QLineEdit(this); m_query->setPlaceholderText(tr("Search name, subtitle, description or objects…")); layout->addWidget(m_query); m_list = new QListWidget(this); layout->addWidget(m_list); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); layout->addWidget(buttons); connect(m_query, &QLineEdit::textChanged, this, [this] { refilter(); }); connect(m_list, &QListWidget::itemActivated, this, [this] { accept(); }); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); refilter(); m_query->setFocus(); } int selectedRoomId() const { QListWidgetItem *item = m_list->currentItem(); return item ? item->data(Qt::UserRole).toInt() : -1; } private: void refilter() { const QString q = m_query->text().trimmed(); m_list->clear(); for (const Room &r : m_map.rooms) { if (!q.isEmpty()) { const bool match = r.name.contains(q, Qt::CaseInsensitive) || r.subtitle.contains(q, Qt::CaseInsensitive) || r.description.contains(q, Qt::CaseInsensitive) || r.objectsText.contains(q, Qt::CaseInsensitive); if (!match) continue; } const QString label = r.name.isEmpty() ? tr("(unnamed room %1)").arg(r.id) : r.name; auto *item = new QListWidgetItem( r.subtitle.isEmpty() ? label : QStringLiteral("%1 — %2").arg(label, r.subtitle), m_list); item->setData(Qt::UserRole, r.id); } if (m_list->count() > 0) m_list->setCurrentRow(0); } const Map &m_map; QLineEdit *m_query = nullptr; QListWidget *m_list = nullptr; }; } // namespace trizbort #endif // TRIZBORT_QUICKFINDDIALOG_H trizbort-qt-1.0.1/src/RoomDialog.cpp000066400000000000000000000357541526521612000173570ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "RoomDialog.h" #include #include #include #include #include #include #include #include #include #include #include "ColorButton.h" namespace trizbort { namespace { // Border tokens exactly as stored in the .trizbort file (BorderDashStyle names). const char *const kBorderStyles[] = {"Solid", "Dash", "DashDot", "DashDotDot", "Dot", "None"}; // Second-fill layouts, matching the C# SecondFillLocation values. const char *const kSecondFillLocations[] = {"Bottom", "BottomRight", "BottomLeft", "Left", "Right", "TopRight", "TopLeft", "Top"}; } // namespace RoomDialog::RoomDialog(const Map &map, const Room &room, QWidget *parent) : QDialog(parent) , m_room(room) { setWindowTitle(tr("Room Properties")); auto *tabs = new QTabWidget(this); // --- General tab --- auto *general = new QWidget(tabs); auto *form = new QFormLayout(general); m_name = new QLineEdit(room.name, general); form->addRow(tr("&Name:"), m_name); m_subtitle = new QLineEdit(room.subtitle, general); form->addRow(tr("&Subtitle:"), m_subtitle); m_region = new QComboBox(general); m_region->setEditable(true); m_region->addItem(kNoRegion); for (const Region &r : map.regions) { if (r.name != kNoRegion) m_region->addItem(r.name); } { const QString cur = room.region.isEmpty() ? kNoRegion : room.region; int idx = m_region->findText(cur); if (idx < 0) { m_region->addItem(cur); idx = m_region->findText(cur); } m_region->setCurrentIndex(idx); } form->addRow(tr("&Region:"), m_region); m_reference = new QComboBox(general); m_reference->addItem(tr("(none)")); for (const Room &r : map.rooms) { if (r.id == room.id) continue; m_reference->addItem(r.name.isEmpty() ? tr("Room %1").arg(r.id) : r.name); m_roomIds.append(r.id); if (r.id == room.referenceRoom) m_reference->setCurrentIndex(m_reference->count() - 1); } form->addRow(tr("Re&ferences room:"), m_reference); m_description = new QPlainTextEdit(room.description, general); m_description->setTabChangesFocus(true); m_description->setFixedHeight(64); form->addRow(tr("&Description:"), m_description); m_objects = new QPlainTextEdit(general); m_objects->setTabChangesFocus(true); m_objects->setFixedHeight(64); QString objects = room.objectsText; objects.replace(QLatin1String("\r\n"), QLatin1String("\n")); m_objects->setPlainText(objects); form->addRow(tr("&Objects:"), m_objects); m_objectsPosition = new QComboBox(general); const struct { const char *label; const char *token; } kPositions[] = {{"South", "s"}, {"South-East", "se"}, {"East", "e"}, {"North-East", "ne"}, {"North", "n"}, {"North-West", "nw"}, {"West", "w"}, {"South-West", "sw"}, // "Center" is CompassPoint.WestSouthWest in the C# dialog: // the list is drawn inside the room, below the name. {"Center", "wsw"}}; for (const auto &p : kPositions) m_objectsPosition->addItem(tr(p.label), QString::fromLatin1(p.token)); { const QString cur = room.objectsPosition.at.isEmpty() ? QStringLiteral("s") : room.objectsPosition.at.toLower(); int idx = m_objectsPosition->findData(cur); if (idx < 0) { // Preserve an in-between position token the combo doesn't list. m_objectsPosition->addItem(cur.toUpper(), cur); idx = m_objectsPosition->count() - 1; } m_objectsPosition->setCurrentIndex(idx); } form->addRow(tr("Objects &position:"), m_objectsPosition); // Custom pixel offset applied on top of the compass anchor (C# // ObjectsCustomPosition + Right/Down). m_objectsCustom = new QCheckBox(tr("Custom offset"), general); m_objectsCustom->setChecked(room.objectsPosition.custom); form->addRow(QString(), m_objectsCustom); m_objectsRight = new QSpinBox(general); m_objectsRight->setRange(-9999, 9999); m_objectsRight->setValue(room.objectsPosition.customRight); form->addRow(tr("Offset ri&ght:"), m_objectsRight); m_objectsDown = new QSpinBox(general); m_objectsDown->setRange(-9999, 9999); m_objectsDown->setValue(room.objectsPosition.customDown); form->addRow(tr("Offset do&wn:"), m_objectsDown); auto syncCustom = [this] { const bool on = m_objectsCustom->isChecked(); m_objectsRight->setEnabled(on); m_objectsDown->setEnabled(on); }; connect(m_objectsCustom, &QCheckBox::toggled, this, syncCustom); syncCustom(); m_dark = new QCheckBox(tr("Dark"), general); m_dark->setChecked(room.isDark); form->addRow(QString(), m_dark); m_start = new QCheckBox(tr("Start room"), general); m_start->setChecked(room.isStartRoom); form->addRow(QString(), m_start); m_end = new QCheckBox(tr("End room"), general); m_end->setChecked(room.isEndRoom); form->addRow(QString(), m_end); tabs->addTab(general, tr("&General")); // --- Appearance tab --- auto *appearance = new QWidget(tabs); auto *aform = new QFormLayout(appearance); m_shape = new QComboBox(appearance); m_shape->addItems({tr("Square"), tr("Rounded"), tr("Ellipse"), tr("Octagonal")}); int shapeIndex = 0; if (room.ellipse) shapeIndex = 2; else if (room.octagonal) shapeIndex = 3; else if (room.roundedCorners) shapeIndex = 1; m_shape->setCurrentIndex(shapeIndex); aform->addRow(tr("S&hape:"), m_shape); // Hand-drawn edges apply to straight-edged (square) rooms only, matching // the C# dialog which enables the checkbox for that shape. m_handDrawn = new QCheckBox(tr("Hand-drawn edges"), appearance); m_handDrawn->setChecked(room.handDrawn); aform->addRow(QString(), m_handDrawn); // Four independent corner radii with an "all equal" toggle, mirroring the // C# per-corner spinners + chkCornersSame (used by the Rounded shape). m_cornersEqual = new QCheckBox(tr("Make all corners equal"), appearance); m_cornersEqual->setChecked(room.allCornersEqual); aform->addRow(QString(), m_cornersEqual); auto makeCorner = [&](double v) { auto *s = new QSpinBox(appearance); s->setRange(static_cast(kMinCornerRadius), static_cast(kMaxCornerRadius)); s->setValue(qRound(clampCornerRadius(v))); return s; }; m_cornerTL = makeCorner(room.cornerTopLeft); m_cornerTR = makeCorner(room.cornerTopRight); m_cornerBL = makeCorner(room.cornerBottomLeft); m_cornerBR = makeCorner(room.cornerBottomRight); aform->addRow(tr("Corner: top-&left:"), m_cornerTL); aform->addRow(tr("top-r&ight:"), m_cornerTR); aform->addRow(tr("bottom-l&eft:"), m_cornerBL); aform->addRow(tr("bottom-ri&ght:"), m_cornerBR); // When "all equal" is on, the other three follow the top-left value and are // disabled; the shape combo enables corners for Rounded and hand-drawn for // Square, as the C# draw-type change handler does. auto syncCornersEqual = [this] { const bool eq = m_cornersEqual->isChecked(); if (eq) { const int v = m_cornerTL->value(); m_cornerTR->setValue(v); m_cornerBL->setValue(v); m_cornerBR->setValue(v); } const bool rounded = (m_shape->currentIndex() == 1); m_cornerTL->setEnabled(rounded); m_cornerTR->setEnabled(rounded && !eq); m_cornerBL->setEnabled(rounded && !eq); m_cornerBR->setEnabled(rounded && !eq); m_cornersEqual->setEnabled(rounded); }; connect(m_cornersEqual, &QCheckBox::toggled, this, syncCornersEqual); connect(m_cornerTL, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { if (m_cornersEqual->isChecked()) { m_cornerTR->setValue(v); m_cornerBL->setValue(v); m_cornerBR->setValue(v); } }); connect(m_shape, QOverload::of(&QComboBox::currentIndexChanged), this, [this, syncCornersEqual] { m_handDrawn->setEnabled(m_shape->currentIndex() == 0); // straight edges only syncCornersEqual(); }); m_handDrawn->setEnabled(m_shape->currentIndex() == 0); syncCornersEqual(); m_border = new QComboBox(appearance); for (const char *style : kBorderStyles) m_border->addItem(QString::fromLatin1(style)); { int idx = m_border->findText(room.borderStyle); m_border->setCurrentIndex(idx < 0 ? 0 : idx); } aform->addRow(tr("&Border style:"), m_border); m_fillButton = new ColorButton(appearance); m_fillButton->setTitle(tr("Room Fill Color")); m_fillButton->setColor(room.fill); aform->addRow(tr("&Fill color:"), m_fillButton); m_secondFillButton = new ColorButton(appearance); m_secondFillButton->setTitle(tr("Second Fill Color")); m_secondFillButton->setColor(room.secondFill); aform->addRow(tr("Second &fill:"), m_secondFillButton); m_secondFillLocation = new QComboBox(appearance); for (const char *loc : kSecondFillLocations) m_secondFillLocation->addItem(QString::fromLatin1(loc)); { int idx = m_secondFillLocation->findText(room.secondFillLocation); m_secondFillLocation->setCurrentIndex(idx < 0 ? 0 : idx); } aform->addRow(tr("Second fill &location:"), m_secondFillLocation); m_borderColorButton = new ColorButton(appearance); m_borderColorButton->setTitle(tr("Border Color")); m_borderColorButton->setColor(room.border); aform->addRow(tr("Border co&lour:"), m_borderColorButton); m_nameColorButton = new ColorButton(appearance); m_nameColorButton->setTitle(tr("Name Text Color")); m_nameColorButton->setColor(room.largeText); aform->addRow(tr("Nam&e color:"), m_nameColorButton); m_subtitleColorButton = new ColorButton(appearance); m_subtitleColorButton->setTitle(tr("Subtitle Color")); m_subtitleColorButton->setColor(room.subtitleColor); aform->addRow(tr("S&ubtitle color:"), m_subtitleColorButton); m_objectColorButton = new ColorButton(appearance); m_objectColorButton->setTitle(tr("Object Text Color")); m_objectColorButton->setColor(room.smallText); aform->addRow(tr("Ob&ject color:"), m_objectColorButton); tabs->addTab(appearance, tr("&Appearance")); auto *outer = new QVBoxLayout(this); outer->addWidget(tabs); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); outer->addWidget(buttons); m_name->setFocus(); m_name->selectAll(); } Room RoomDialog::result() const { Room r = m_room; r.name = m_name->text(); r.subtitle = m_subtitle->text(); r.region = normalizeRegionName(m_region->currentText().trimmed()); r.description = m_description->toPlainText(); const int refIdx = m_reference->currentIndex(); r.referenceRoom = (refIdx > 0 && refIdx - 1 < m_roomIds.size()) ? m_roomIds.at(refIdx - 1) : -1; QString objects = m_objects->toPlainText(); objects.replace(QLatin1String("\r\n"), QLatin1String("\n")); r.objectsText = objects; r.objectsPosition.at = m_objectsPosition->currentData().toString(); r.objectsPosition.custom = m_objectsCustom->isChecked(); r.objectsPosition.customRight = m_objectsRight->value(); r.objectsPosition.customDown = m_objectsDown->value(); r.isDark = m_dark->isChecked(); r.isStartRoom = m_start->isChecked(); r.isEndRoom = m_end->isChecked(); r.ellipse = r.octagonal = r.roundedCorners = false; switch (m_shape->currentIndex()) { case 1: r.roundedCorners = true; break; case 2: r.ellipse = true; break; case 3: r.octagonal = true; break; default: break; } r.handDrawn = m_handDrawn->isChecked(); r.allCornersEqual = m_cornersEqual->isChecked(); r.cornerTopLeft = m_cornerTL->value(); r.cornerTopRight = m_cornerTR->value(); r.cornerBottomLeft = m_cornerBL->value(); r.cornerBottomRight = m_cornerBR->value(); r.borderStyle = m_border->currentText(); r.fill = m_fillButton->color(); r.border = m_borderColorButton->color(); r.secondFill = m_secondFillButton->color(); r.secondFillLocation = m_secondFillLocation->currentText(); r.largeText = m_nameColorButton->color(); r.subtitleColor = m_subtitleColorButton->color(); r.smallText = m_objectColorButton->color(); return r; } } // namespace trizbort trizbort-qt-1.0.1/src/RoomDialog.h000066400000000000000000000100251526521612000170040ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_ROOMDIALOG_H #define TRIZBORT_ROOMDIALOG_H #include #include "MapDocument.h" class QLineEdit; class QPlainTextEdit; class QComboBox; class QCheckBox; class QSpinBox; namespace trizbort { class ColorButton; // Modal editor for a room's properties. Constructed from a copy of the room; // result() returns the edited room (id, seq and geometry preserved). class RoomDialog : public QDialog { Q_OBJECT public: RoomDialog(const Map &map, const Room &room, QWidget *parent = nullptr); Room result() const; private: Room m_room; QLineEdit *m_name = nullptr; QLineEdit *m_subtitle = nullptr; QComboBox *m_region = nullptr; QComboBox *m_reference = nullptr; QPlainTextEdit *m_description = nullptr; QPlainTextEdit *m_objects = nullptr; QComboBox *m_objectsPosition = nullptr; QCheckBox *m_objectsCustom = nullptr; QSpinBox *m_objectsRight = nullptr; QSpinBox *m_objectsDown = nullptr; QCheckBox *m_dark = nullptr; QCheckBox *m_start = nullptr; QCheckBox *m_end = nullptr; QComboBox *m_shape = nullptr; QCheckBox *m_handDrawn = nullptr; QComboBox *m_border = nullptr; QCheckBox *m_cornersEqual = nullptr; QSpinBox *m_cornerTL = nullptr; QSpinBox *m_cornerTR = nullptr; QSpinBox *m_cornerBL = nullptr; QSpinBox *m_cornerBR = nullptr; QComboBox *m_secondFillLocation = nullptr; ColorButton *m_fillButton = nullptr; ColorButton *m_borderColorButton = nullptr; ColorButton *m_secondFillButton = nullptr; ColorButton *m_nameColorButton = nullptr; ColorButton *m_subtitleColorButton = nullptr; ColorButton *m_objectColorButton = nullptr; QList m_roomIds; // parallels m_reference entries (after the "(none)" item) }; } // namespace trizbort #endif // TRIZBORT_ROOMDIALOG_H trizbort-qt-1.0.1/src/RoomItem.cpp000066400000000000000000000776021526521612000170540ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "RoomItem.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "AppSettings.h" #include "EditCommands.h" #include "FontUtil.h" #include "MapScene.h" namespace trizbort { namespace { // Append a hand-drawn (jittered) edge from a to b to a path whose current point // is already a, matching Drawing.AddLine: the edge is split into a handful of // sub-segments whose interior points wander by +/-1px, giving the sketchy look. void addHandDrawnEdge(QPainterPath &path, const QPointF &a, const QPointF &b, std::mt19937 &rng) { auto nextInt = [&rng](int lo, int hiExclusive) { if (hiExclusive <= lo) return lo; return std::uniform_int_distribution(lo, hiExclusive - 1)(rng); }; const double dx = b.x() - a.x(); const double dy = b.y() - a.y(); const double dist = std::hypot(dx, dy); int lo = std::max(3, static_cast(dist / 15.0)); int hi = std::max(6, static_cast(dist / 8.0)); if (hi <= lo) hi = lo + 1; const int lines = nextInt(lo, hi) - 1; for (int line = 0; line < lines; ++line) { QPointF next; if (line == 0) next = a; else if (line == lines - 1) next = b; else { const double f = static_cast(line) / (lines - 1); next = QPointF(a.x() + dx * f + nextInt(-1, 2), a.y() + dy * f + nextInt(-1, 2)); } path.lineTo(next); } } // A per-room deterministic RNG, seeded from the room name so the sketch is // stable across repaints (as the C# renderer seeds Random(Name.GetHashCode)). std::mt19937 handDrawnRng(const Room &room) { unsigned int hash = 2166136261u; for (const QChar c : room.name) { hash ^= c.unicode(); hash *= 16777619u; } return std::mt19937(hash ^ 0x9e3779b9u); } // Local-coordinate center of resize handle i (0=N,1=NE,2=E,3=SE,4=S,5=SW,6=W, // 7=NW) on a room of the given size. QPointF handlePoint(int i, double w, double h) { switch (i) { case 0: return {w / 2.0, 0}; case 1: return {w, 0}; case 2: return {w, h / 2.0}; case 3: return {w, h}; case 4: return {w / 2.0, h}; case 5: return {0, h}; case 6: return {0, h / 2.0}; case 7: return {0, 0}; default: return {0, 0}; } } // The text color a region contributes for room names (invalid if unknown). QColor regionTextColor(const Map &map, const QString ®ion) { if (const Region *rg = map.regionByName(region)) return rg->textColor; return QColor(); } // The Qt pen style for a stored border token, or Qt::NoPen for "None". Qt::PenStyle penStyleForBorder(const QString &style) { if (style == QLatin1String("None")) return Qt::NoPen; if (style == QLatin1String("Dot")) return Qt::DotLine; if (style == QLatin1String("Dash") || style == QLatin1String("Dashed") || style == QLatin1String("Custom")) return Qt::DashLine; if (style == QLatin1String("DashDot")) return Qt::DashDotLine; if (style == QLatin1String("DashDotDot")) return Qt::DashDotDotLine; return Qt::SolidLine; } // Build the outline path for a room's shape within the given rectangle: ellipse, // octagon (independent per-axis quarter chamfers), rounded (per-corner radii) or // plain rectangle. Mirrors Room.createRoomPath / the octagon quarter-points. QPainterPath buildRoomPath(const Room &room, const QRectF &r) { QPainterPath path; if (room.ellipse) { path.addEllipse(r); return path; } if (room.octagonal) { const double cx = r.width() * 0.25; const double cy = r.height() * 0.25; QVector v{ QPointF(r.left() + cx, r.top()), QPointF(r.right() - cx, r.top()), QPointF(r.right(), r.top() + cy), QPointF(r.right(), r.bottom() - cy), QPointF(r.right() - cx, r.bottom()), QPointF(r.left() + cx, r.bottom()), QPointF(r.left(), r.bottom() - cy), QPointF(r.left(), r.top() + cy)}; if (room.handDrawn) { std::mt19937 rng = handDrawnRng(room); path.moveTo(v.first()); for (int i = 0; i < v.size(); ++i) addHandDrawnEdge(path, v.at(i), v.at((i + 1) % v.size()), rng); } else { path.addPolygon(QPolygonF(v)); } path.closeSubpath(); return path; } if (room.roundedCorners) { const double maxR = qMin(r.width(), r.height()) / 2.0; const double tl = qBound(0.0, room.cornerTopLeft, maxR); const double tr = qBound(0.0, room.cornerTopRight, maxR); const double br = qBound(0.0, room.cornerBottomRight, maxR); const double bl = qBound(0.0, room.cornerBottomLeft, maxR); path.moveTo(r.left() + tl, r.top()); path.lineTo(r.right() - tr, r.top()); if (tr > 0.0) path.arcTo(r.right() - 2 * tr, r.top(), 2 * tr, 2 * tr, 90, -90); path.lineTo(r.right(), r.bottom() - br); if (br > 0.0) path.arcTo(r.right() - 2 * br, r.bottom() - 2 * br, 2 * br, 2 * br, 0, -90); path.lineTo(r.left() + bl, r.bottom()); if (bl > 0.0) path.arcTo(r.left(), r.bottom() - 2 * bl, 2 * bl, 2 * bl, 270, -90); path.lineTo(r.left(), r.top() + tl); if (tl > 0.0) path.arcTo(r.left(), r.top(), 2 * tl, 2 * tl, 180, -90); path.closeSubpath(); return path; } if (room.handDrawn) { std::mt19937 rng = handDrawnRng(room); const QPointF tl = r.topLeft(); const QPointF tr = r.topRight(); const QPointF br = r.bottomRight(); const QPointF bl = r.bottomLeft(); path.moveTo(tl); addHandDrawnEdge(path, tl, tr, rng); addHandDrawnEdge(path, tr, br, rng); addHandDrawnEdge(path, br, bl, rng); addHandDrawnEdge(path, bl, tl, rng); path.closeSubpath(); return path; } path.addRect(r); return path; } // Build the closed sub-region filled by a room's second fill, matching the eight // SecondFillLocation layouts in Room.Draw (half-and-half splits and corners). QPainterPath buildSecondFillPath(const Room &room, const QRectF &r) { const double w = r.width(); const double h = r.height(); const QPointF tl(r.left(), r.top()); const QPointF tr(r.right(), r.top()); const QPointF bl(r.left(), r.bottom()); const QPointF br(r.right(), r.bottom()); const QPointF tc(r.left() + w / 2.0, r.top()); const QPointF rc(r.right(), r.top() + h / 2.0); const QPointF bc(r.left() + w / 2.0, r.bottom()); const QPointF lc(r.left(), r.top() + h / 2.0); QPolygonF poly; const QString loc = room.secondFillLocation; if (loc == QLatin1String("Bottom")) poly << lc << rc << br << bl; else if (loc == QLatin1String("Top")) poly << lc << rc << tr << tl; else if (loc == QLatin1String("Left")) poly << tc << tl << bl << bc; else if (loc == QLatin1String("Right")) poly << tc << tr << br << bc; else if (loc == QLatin1String("BottomRight")) poly << bl << tr << br; else if (loc == QLatin1String("BottomLeft")) poly << tl << bl << br; else if (loc == QLatin1String("TopRight")) poly << tl << tr << br; else if (loc == QLatin1String("TopLeft")) poly << tl << tr << bl; else poly << lc << rc << br << bl; // default: Bottom QPainterPath path; path.addPolygon(poly); path.closeSubpath(); return path; } // Draw a room's name, subtitle and object list, matching the C# Room.Draw // text layout: the name is centerd in the room using the room font and region // (or room-specific) text color; the subtitle sits just below it; the object // list is anchored at one of the compass positions outside the room (or inside, // below the name, for the in-between positions), with its [..] property tags // stripped. Reference rooms show "To" over the referenced room's name. void drawRoomText(QPainter *painter, const Map &map, const Room &room, const QRectF &rect) { // --- Name + subtitle --- QRectF textBounds = room.ellipse ? rect.adjusted(11.5, 11.5, -11.5, -11.5) : rect.adjusted(5, 5, -5, -5); QString nameText = room.name; QString subtitleText = room.subtitle; if (room.referenceRoom >= 0) { const Room *ref = map.roomById(room.referenceRoom); nameText = QObject::tr("To"); subtitleText = ref ? ref->name : QString(); } nameText = applyDashWrapping(nameText, map.settings.wrapTextAtDashes); subtitleText = applyDashWrapping(subtitleText, map.settings.wrapTextAtDashes); if (textBounds.width() > 0 && textBounds.height() > 0 && !nameText.isEmpty()) { QColor nameColor = room.largeText.isValid() ? room.largeText : regionTextColor(map, room.region); if (!nameColor.isValid()) nameColor = QColor(Qt::black); const QFont nameFont = qfontFromSpec(map.settings.roomFont, 13.0); painter->setFont(nameFont); painter->setPen(nameColor); const QRectF nameBox = painter->boundingRect( textBounds, Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextWordWrap, nameText); painter->drawText(textBounds, Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextWordWrap, nameText); if (!subtitleText.isEmpty()) { QColor subColor = room.subtitleColor.isValid() ? room.subtitleColor : map.settings.colors[ColorSubtitle]; if (!subColor.isValid()) subColor = QColor(90, 90, 90); const QFont subFont = qfontFromSpec(map.settings.subtitleFont, 9.0); painter->setFont(subFont); painter->setPen(subColor); const QRectF subRect(nameBox.left(), nameBox.bottom(), nameBox.width(), textBounds.bottom() - nameBox.bottom()); if (subRect.height() > 0) painter->drawText(subRect, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, subtitleText); } } // --- Object list --- if (room.objectsText.isEmpty()) return; QString objs = room.objectsText; objs.remove(QRegularExpression(QStringLiteral("\\[[^\\]\\[]*\\]"))); objs = objs.trimmed(); if (objs.isEmpty()) return; objs = applyDashWrapping(objs, map.settings.wrapTextAtDashes); const double off = map.settings.objectListOffset; QColor objColor = room.smallText.isValid() ? room.smallText : map.settings.colors[ColorSmallText]; if (!objColor.isValid()) objColor = QColor(60, 60, 60); const QFont objFont = qfontFromSpec(map.settings.objectFont, 11.0); painter->setFont(objFont); painter->setPen(objColor); const QFontMetricsF ofm(objFont); const QRectF ob = ofm.boundingRect(QRectF(0, 0, 4000, 4000), Qt::AlignLeft | Qt::TextWordWrap, objs); const double ow = ob.width(); const double oh = ob.height(); const QString tok = room.objectsPosition.at.isEmpty() ? QStringLiteral("s") : room.objectsPosition.at.toLower(); const double dx = room.objectsPosition.custom ? room.objectsPosition.customRight : 0; const double dy = room.objectsPosition.custom ? room.objectsPosition.customDown : 0; // The "in-between" compass positions draw the list inside the room, below // the name (matching SetAlignmentFromCardinalOrOrdinalDirection returning // false for those directions). const bool inside = (tok == QLatin1String("nne") || tok == QLatin1String("ene") || tok == QLatin1String("ese") || tok == QLatin1String("sse") || tok == QLatin1String("ssw") || tok == QLatin1String("wsw") || tok == QLatin1String("wnw")); if (inside) { const double height = rect.height() / 2.0 - ofm.height() / 2.0; QRectF b(rect.left() + off, rect.bottom() - height, rect.width() - off, height - off); b.translate(dx, dy); if (b.width() > 0 && b.height() > 0) painter->drawText(b, Qt::AlignLeft | Qt::AlignBottom | Qt::TextWordWrap, objs); return; } // Otherwise anchor the list at a compass corner of the room inflated by the // object-list offset, growing away from the room. QPointF pos = MapScene::squareCorner(rect.left() - off, rect.top() - off, rect.width() + 2 * off, rect.height() + 2 * off, tok); if (tok == QLatin1String("n") || tok == QLatin1String("s")) pos.rx() += off; pos += QPointF(dx, dy); // Horizontal: grow right except for the west-ish anchors, which grow left. const bool growLeft = (tok == QLatin1String("w") || tok == QLatin1String("sw") || tok == QLatin1String("nw") || tok == QLatin1String("nnw")); // Vertical: grow up for the north-ish anchors, down otherwise. const bool growUp = (tok == QLatin1String("n") || tok == QLatin1String("ne") || tok == QLatin1String("nw") || tok == QLatin1String("nnw")); const double left = growLeft ? pos.x() - ow : pos.x(); const double top = growUp ? pos.y() - oh : pos.y(); painter->drawText(QRectF(left, top, ow, oh), Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip | Qt::TextWordWrap, objs); } } // namespace RoomItem::RoomItem(MapScene *scene, int roomId) : m_scene(scene) , m_roomId(roomId) { setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges); setAcceptHoverEvents(true); // for resize-handle cursors setZValue(0); syncFromModel(); } void RoomItem::syncFromModel() { const Map *map = m_scene->document(); const Room *room = map ? map->roomById(m_roomId) : nullptr; if (!room) return; m_applyingModel = true; m_w = room->w; m_h = room->h; setZValue(room->zOrder); // honor per-room stacking order (connections are at -1) prepareGeometryChange(); setPos(room->x, room->y); m_applyingModel = false; // Hover tooltip: name, subtitle, and — per the application settings — the // description and object list (optionally truncated). const AppSettings &app = AppSettings::instance(); QString tip = room->name.isEmpty() ? QObject::tr("(unnamed room)") : room->name; if (!room->subtitle.isEmpty()) tip += QLatin1Char('\n') + room->subtitle; if (app.showDescriptionsInTooltips && !room->description.trimmed().isEmpty()) { QString desc = room->description.trimmed(); if (app.limitRoomDescriptionChars && app.roomDescriptionChars > 0 && desc.length() > app.roomDescriptionChars) desc = desc.left(app.roomDescriptionChars).trimmed() + QStringLiteral("…"); tip += QStringLiteral("\n\n") + desc; } if (app.showObjectsInTooltips) { QString objs = room->objectsText; objs.remove(QRegularExpression(QStringLiteral("\\[[^\\]\\[]*\\]"))); objs = objs.trimmed(); if (!objs.isEmpty()) tip += QObject::tr("\n\nObjects:\n") + objs; } setToolTip(tip); update(); } QRectF RoomItem::boundingRect() const { // Padded generously so subtitle text and the object list drawn around the // room (in any compass direction) fall inside the item's update region. const double margin = 90.0; return QRectF(-margin, -margin, m_w + 2 * margin, m_h + 2 * margin); } QPainterPath RoomItem::shape() const { QPainterPath p; p.addRect(QRectF(0, 0, m_w, m_h)); return p; } void RoomItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { const Map *map = m_scene->document(); const Room *room = map ? map->roomById(m_roomId) : nullptr; if (!room) return; const QRectF rect(0, 0, m_w, m_h); const bool isReference = room->referenceRoom >= 0; QColor fill = room->fill.isValid() ? room->fill : map->regionFill(room->region); if (!fill.isValid()) fill = QColor(Qt::white); QColor border = room->border.isValid() ? room->border : map->borderColor(); if (!border.isValid()) border = QColor(Qt::black); const QPainterPath path = buildRoomPath(*room, rect); painter->setRenderHint(QPainter::Antialiasing, true); // Start / end / reference rooms get an inflated outline that follows the // room's shape (green/red from the palette; light blue for a reference). if (room->isStartRoom || room->isEndRoom || isReference) { const QPainterPath halo = buildRoomPath(*room, rect.adjusted(-5, -5, 5, 5)); QColor haloColor = isReference ? QColor(173, 216, 230) : (room->isStartRoom ? map->settings.colors[ColorStartRoom] : map->settings.colors[ColorEndRoom]); if (!haloColor.isValid()) haloColor = room->isStartRoom ? QColor(Qt::green) : QColor(Qt::red); painter->setPen(QPen(haloColor, 2.0)); painter->setBrush(Qt::NoBrush); painter->drawPath(halo); } // The room outline, fill, second fill and darkness stripe are only drawn // when the border style isn't "None" (matching Room.Draw, where a None // border suppresses the whole shape and leaves only the text). if (room->borderStyle != QLatin1String("None")) { // Fill the interior. painter->setPen(Qt::NoPen); painter->setBrush(fill); painter->drawPath(path); // Second fill: a colored sub-region, clipped to the room shape. if (room->secondFill.isValid()) { painter->save(); painter->setClipPath(path); painter->setPen(Qt::NoPen); painter->setBrush(room->secondFill); painter->drawPath(buildSecondFillPath(*room, rect)); painter->restore(); } // Darkness stripe: a corner triangle drawn in the border color, sized // from the darkness-stripe setting and adjusted per shape. if (room->isDark) { double dx = map->settings.darknessStripeSize; double dy = map->settings.darknessStripeSize; if (room->ellipse) { dx = 2.0 * m_w / 5.0; dy = 2.0 * m_h / 5.0; } else if (room->octagonal) { dx = m_w * 7.0 / 20.0; dy = m_h * 7.0 / 20.0; } else if (room->roundedCorners && room->cornerTopRight > 2.0 * map->settings.darknessStripeSize) { dx = dy = room->cornerTopRight / 2.0; } painter->save(); painter->setClipPath(path); painter->setPen(QPen(border, map->settings.lineWidth > 0 ? map->settings.lineWidth : 2.0)); painter->setBrush(Qt::NoBrush); QPolygonF tri; tri << QPointF(m_w, 0) << QPointF(m_w - dx, 0) << QPointF(m_w, dy); painter->drawPolygon(tri); painter->restore(); } // The border on top. Reference rooms always use a dotted border. QPen pen(border); pen.setWidthF(map->settings.lineWidth > 0 ? map->settings.lineWidth : 2.0); pen.setCapStyle(Qt::RoundCap); pen.setStyle(isReference ? Qt::DotLine : penStyleForBorder(room->borderStyle)); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(path); } if (m_scene->textVisible()) drawRoomText(painter, *map, *room, rect); // Validation: a red X across rooms that fail the active validation rules. if (m_scene->roomInvalid(*room)) { painter->setPen(QPen(QColor(Qt::red), 2.0)); painter->setBrush(Qt::NoBrush); painter->drawLine(rect.topLeft(), rect.bottomRight()); painter->drawLine(rect.topRight(), rect.bottomLeft()); } // Selection highlight: a gold outline following the room shape, plus the // eight compass resize handles. if (isSelected()) { const QPainterPath sel = buildRoomPath(*room, rect.adjusted(-5, -5, 5, 5)); painter->setPen(QPen(QColor(255, 215, 0), 2.0)); painter->setBrush(Qt::NoBrush); painter->drawPath(sel); const double hs = handleSize(); painter->setPen(QPen(QColor(80, 80, 80), 1.0)); for (int i = 0; i < 8; ++i) { const QPointF c = handlePoint(i, m_w, m_h); const QRectF hr(c.x() - hs / 2.0, c.y() - hs / 2.0, hs, hs); // Light-cyan-to-steel-blue gradient, matching the C# resize handles. QLinearGradient g(hr.topLeft(), hr.bottomRight()); g.setColorAt(0.0, QColor(224, 255, 255)); g.setColorAt(1.0, QColor(70, 130, 180)); painter->setBrush(g); painter->drawRect(hr); } } // In connect mode, show the compass docking ports on the hovered room so the // user can see (and pick) where a connection will attach — the C# drawPorts. if (m_hovered && m_scene->connectMode()) { const QStringList ports = MapScene::portTokensForDetail(AppSettings::instance().portAdjustDetail); for (const QString &t : ports) { const QPointF wp = MapScene::portPoint(*room, t); const QPointF lp(wp.x() - room->x, wp.y() - room->y); const bool hot = (t == m_hoverPort); painter->setPen(QPen(QColor(30, 120, 220), hot ? 2.0 : 1.0)); painter->setBrush(hot ? QColor(30, 120, 220) : QColor(200, 225, 255)); const double r = hot ? 6.0 : 3.5; painter->drawEllipse(lp, r, r); } } } QVariant RoomItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && scene() && !m_applyingModel) { // Snap the proposed top-left to the grid. return m_scene->snap(value.toPointF()); } if (change == ItemPositionHasChanged && !m_applyingModel) { m_scene->roomMovedTo(m_roomId, pos()); } return QGraphicsItem::itemChange(change, value); } void RoomItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { m_scene->activateRoom(m_roomId); event->accept(); } void RoomItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if (!isSelected()) { if (scene()) scene()->clearSelection(); setSelected(true); } QMenu menu; QAction *editAct = menu.addAction(QObject::tr("Edit…")); QAction *deleteAct = menu.addAction(QObject::tr("Delete")); QAction *chosen = menu.exec(event->screenPos()); if (chosen == editAct) m_scene->activateRoom(m_roomId); else if (chosen == deleteAct) m_scene->deleteSelectionLater(); event->accept(); } double RoomItem::handleSize() const { if (const Map *map = m_scene ? m_scene->document() : nullptr) if (map->settings.handleSize > 1.0) return map->settings.handleSize; return 10.0; } int RoomItem::handleAt(const QPointF &localPos) const { if (!isSelected()) return -1; const double tol = handleSize() / 2.0 + 2.0; for (int i = 0; i < 8; ++i) { const QPointF c = handlePoint(i, m_w, m_h); if (std::abs(localPos.x() - c.x()) <= tol && std::abs(localPos.y() - c.y()) <= tol) return i; } return -1; } void RoomItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { m_hovered = true; if (m_scene->connectMode()) { updateHoverPort(event->scenePos()); update(); } QGraphicsItem::hoverEnterEvent(event); } void RoomItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { const int h = handleAt(event->pos()); if (h == 0 || h == 4) setCursor(Qt::SizeVerCursor); else if (h == 2 || h == 6) setCursor(Qt::SizeHorCursor); else if (h == 1 || h == 5) setCursor(Qt::SizeBDiagCursor); else if (h == 3 || h == 7) setCursor(Qt::SizeFDiagCursor); else unsetCursor(); if (m_scene->connectMode()) updateHoverPort(event->scenePos()); QGraphicsItem::hoverMoveEvent(event); } void RoomItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { unsetCursor(); const bool wasConnect = m_hovered && m_scene->connectMode(); m_hovered = false; m_hoverPort.clear(); if (wasConnect) update(); QGraphicsItem::hoverLeaveEvent(event); } void RoomItem::updateHoverPort(const QPointF &scenePos) { const Map *map = m_scene ? m_scene->document() : nullptr; const Room *room = map ? map->roomById(m_roomId) : nullptr; if (!room) return; const QString p = MapScene::nearestPort(*room, scenePos, AppSettings::instance().portAdjustDetail); if (p != m_hoverPort) { m_hoverPort = p; update(); } } void RoomItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { // Ctrl+click on a reference room jumps to (selects and reveals) the room it // references, matching the C# Canvas.OnMouseClick behavior. if (event->button() == Qt::LeftButton && (event->modifiers() & Qt::ControlModifier)) { const Room *r = m_scene->document() ? m_scene->document()->roomById(m_roomId) : nullptr; if (r && r->referenceRoom >= 0) { const int target = r->referenceRoom; m_scene->selectRoomItem(target); if (const Room *tr = m_scene->document()->roomById(target)) { const QList views = m_scene->views(); if (!views.isEmpty()) views.first()->ensureVisible(QRectF(tr->x, tr->y, tr->w, tr->h)); } event->accept(); return; } } if (event->button() == Qt::LeftButton) { const int h = handleAt(event->pos()); if (h >= 0) { m_resizeHandle = h; m_resizeStartScene = event->scenePos(); if (const Room *r = m_scene->document()->roomById(m_roomId)) { m_startX = r->x; m_startY = r->y; m_startW = r->w; m_startH = r->h; } m_scene->setRoomResizeActive(true); // suppress the move-undo snapshot event->accept(); return; } } QGraphicsItem::mousePressEvent(event); } void RoomItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_resizeHandle >= 0) { const int i = m_resizeHandle; const QPointF d = event->scenePos() - m_resizeStartScene; double x = m_startX, y = m_startY, w = m_startW, h = m_startH; if (i == 1 || i == 2 || i == 3) w = m_startW + d.x(); // right edge if (i == 5 || i == 6 || i == 7) { x = m_startX + d.x(); w = m_startW - d.x(); // left edge } if (i == 3 || i == 4 || i == 5) h = m_startH + d.y(); // bottom edge if (i == 0 || i == 1 || i == 7) { y = m_startY + d.y(); h = m_startH - d.y(); // top edge } const double minS = 16.0; const bool left = (i == 5 || i == 6 || i == 7); const bool top = (i == 0 || i == 1 || i == 7); if (w < minS) { if (left) x = m_startX + m_startW - minS; w = minS; } if (h < minS) { if (top) y = m_startY + m_startH - minS; h = minS; } if (Room *r = m_scene->document()->roomById(m_roomId)) { r->x = x; r->y = y; r->w = w; r->h = h; m_w = w; m_h = h; m_applyingModel = true; prepareGeometryChange(); setPos(x, y); m_applyingModel = false; update(); m_scene->refreshConnectionsFor(m_roomId); } event->accept(); return; } QGraphicsItem::mouseMoveEvent(event); } void RoomItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (m_resizeHandle >= 0) { m_resizeHandle = -1; m_scene->setRoomResizeActive(false); Room *r = m_scene->document() ? m_scene->document()->roomById(m_roomId) : nullptr; if (r) { // Snap the edges to the grid if snapping is on. if (m_scene->document()->settings.snapToGrid) { const double g = m_scene->gridSize(); const double nx = std::round(r->x / g) * g; const double ny = std::round(r->y / g) * g; const double nr = std::round((r->x + r->w) / g) * g; const double nb = std::round((r->y + r->h) / g) * g; r->x = nx; r->y = ny; r->w = std::max(g, nr - nx); r->h = std::max(g, nb - ny); m_w = r->w; m_h = r->h; m_applyingModel = true; prepareGeometryChange(); setPos(r->x, r->y); m_applyingModel = false; update(); m_scene->refreshConnectionsFor(m_roomId); } Room before = *r; before.x = m_startX; before.y = m_startY; before.w = m_startW; before.h = m_startH; const Room after = *r; if (m_scene->undoStack() && (after.x != before.x || after.y != before.y || after.w != before.w || after.h != before.h)) { m_scene->undoStack()->push(new EditRoomCommand(m_scene, m_roomId, before, after)); } } event->accept(); return; } QGraphicsItem::mouseReleaseEvent(event); } } // namespace trizbort trizbort-qt-1.0.1/src/RoomItem.h000066400000000000000000000110561526521612000165100ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_ROOMITEM_H #define TRIZBORT_ROOMITEM_H #include #include namespace trizbort { class MapScene; // An interactive room on the editing canvas. Reads its geometry and appearance // from the room in the document (looked up by id) and, when dragged, writes the // new grid-snapped position back through the scene. class RoomItem : public QGraphicsItem { public: RoomItem(MapScene *scene, int roomId); int roomId() const { return m_roomId; } // Re-read geometry from the model and reposition (called after edits). void syncFromModel(); QRectF boundingRect() const override; // The clickable/selectable outline is the room rectangle itself, even though // boundingRect is padded to cover subtitle and object-list text drawn around // the room. QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: double handleSize() const; // Index 0..7 of the resize handle under a local point, or -1. int handleAt(const QPointF &localPos) const; void updateHoverPort(const QPointF &scenePos); MapScene *m_scene; int m_roomId; double m_w = 96.0; double m_h = 64.0; bool m_applyingModel = false; // guard against feedback while syncing bool m_hovered = false; // mouse over this room (for connect-mode ports) QString m_hoverPort; int m_resizeHandle = -1; // active resize handle, or -1 QPointF m_resizeStartScene; // scene pos where the resize drag began double m_startX = 0, m_startY = 0, m_startW = 0, m_startH = 0; }; } // namespace trizbort #endif // TRIZBORT_ROOMITEM_H trizbort-qt-1.0.1/src/SettingsDialog.cpp000066400000000000000000000443141526521612000202330ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "SettingsDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FontUtil.h" namespace trizbort { namespace { struct SlotLabel { ColorSlot slot; const char *label; }; const SlotLabel kSlotLabels[] = { {ColorCanvas, "Canvas"}, {ColorBorder, "Room border"}, {ColorLine, "Connection line"}, {ColorSelectedLine, "Selected line"}, {ColorHoverLine, "Hover line"}, {ColorSubtitle, "Subtitle text"}, {ColorSmallText, "Object text"}, {ColorLineText, "Connection text"}, {ColorGrid, "Grid"}, {ColorStartRoom, "Start room"}, {ColorEndRoom, "End room"}, }; QDoubleSpinBox *makeSpin(double value, double max = 4096.0) { auto *spin = new QDoubleSpinBox; spin->setRange(0, max); spin->setDecimals(2); spin->setValue(value); return spin; } } // namespace SettingsDialog::SettingsDialog(const MapSettings &settings, const QList ®ions, QWidget *parent) : QDialog(parent) , m_settings(settings) , m_regions(regions) { setWindowTitle(tr("Map Settings")); resize(460, 480); m_roomFont = m_settings.roomFont; m_subtitleFont = m_settings.subtitleFont; m_objectFont = m_settings.objectFont; m_lineFont = m_settings.lineFont; auto *layout = new QVBoxLayout(this); auto *tabs = new QTabWidget(this); tabs->addTab(buildColorsTab(), tr("&Colors")); tabs->addTab(buildGridLinesTab(), tr("&Grid && Lines")); tabs->addTab(buildRoomsTab(), tr("&Rooms")); tabs->addTab(buildFontsTab(), tr("&Fonts")); tabs->addTab(buildAdvancedTab(), tr("Ad&vanced")); tabs->addTab(buildRegionsTab(), tr("Re&gions")); layout->addWidget(tabs); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(buttons); } void SettingsDialog::setButtonColor(QPushButton *button, const QColor &color) { const QColor c = color.isValid() ? color : QColor(Qt::white); button->setProperty("swatch", c); button->setText(c.name().toUpper()); const QString fg = (c.lightnessF() < 0.5) ? QStringLiteral("white") : QStringLiteral("black"); button->setStyleSheet( QStringLiteral("background-color: %1; color: %2;").arg(c.name(), fg)); } QColor SettingsDialog::buttonColor(const QPushButton *button) const { return button->property("swatch").value(); } QWidget *SettingsDialog::buildColorsTab() { auto *widget = new QWidget; auto *form = new QFormLayout(widget); for (const SlotLabel &sl : kSlotLabels) { auto *button = new QPushButton(widget); setButtonColor(button, m_settings.colors[sl.slot]); connect(button, &QPushButton::clicked, this, [this, button] { const QColor chosen = QColorDialog::getColor(buttonColor(button), this, tr("Choose color")); if (chosen.isValid()) setButtonColor(button, chosen); }); m_colorButtons[sl.slot] = button; form->addRow(tr(sl.label), button); } return widget; } QWidget *SettingsDialog::buildGridLinesTab() { auto *widget = new QWidget; auto *form = new QFormLayout(widget); m_gridSize = makeSpin(m_settings.gridSize, 512); form->addRow(tr("Grid size:"), m_gridSize); m_gridVisible = new QCheckBox(tr("Show grid"), widget); m_gridVisible->setChecked(m_settings.gridVisible); form->addRow(QString(), m_gridVisible); m_snapToGrid = new QCheckBox(tr("Snap to grid"), widget); m_snapToGrid->setChecked(m_settings.snapToGrid); form->addRow(QString(), m_snapToGrid); m_showOrigin = new QCheckBox(tr("Show origin"), widget); m_showOrigin->setChecked(m_settings.showOrigin); form->addRow(QString(), m_showOrigin); m_lineWidth = makeSpin(m_settings.lineWidth, 64); form->addRow(tr("Line width:"), m_lineWidth); m_arrowSize = makeSpin(m_settings.arrowSize, 128); form->addRow(tr("Arrow size:"), m_arrowSize); m_textOffset = makeSpin(m_settings.textOffset, 128); form->addRow(tr("Text offset:"), m_textOffset); return widget; } QWidget *SettingsDialog::buildRoomsTab() { auto *widget = new QWidget; auto *form = new QFormLayout(widget); m_defaultRoomName = new QLineEdit(m_settings.defaultRoomName, widget); form->addRow(tr("Default room name:"), m_defaultRoomName); m_defaultRoomShape = new QComboBox(widget); m_defaultRoomShape->addItems({tr("Square"), tr("Rounded"), tr("Ellipse"), tr("Octagonal")}); m_defaultRoomShape->setCurrentIndex( qBound(0, m_settings.defaultRoomShape, m_defaultRoomShape->count() - 1)); form->addRow(tr("Default room shape:"), m_defaultRoomShape); m_darknessStripe = makeSpin(m_settings.darknessStripeSize, 256); form->addRow(tr("Darkness stripe size:"), m_darknessStripe); m_objectListOffset = makeSpin(m_settings.objectListOffset, 256); form->addRow(tr("Object list offset:"), m_objectListOffset); m_stalkLength = makeSpin(m_settings.connectionStalkLength, 512); form->addRow(tr("Connection stalk length:"), m_stalkLength); m_preferredDistance = makeSpin(m_settings.preferredDistanceBetweenRooms, 2048); form->addRow(tr("Preferred distance between rooms:"), m_preferredDistance); return widget; } void SettingsDialog::setupFontButton(QPushButton *button, FontSpec *spec) { auto refresh = [button, spec] { const double size = spec->size > 0.0 ? spec->size : 12.0; const QString family = spec->family.isEmpty() ? tr("(default)") : spec->family; button->setText(tr("%1, %2 pt").arg(family).arg(size)); button->setFont(qfontFromSpec(*spec, 10.0)); }; refresh(); connect(button, &QPushButton::clicked, this, [this, spec, refresh] { bool ok = false; const QFont chosen = QFontDialog::getFont(&ok, qfontFromSpec(*spec, 12.0), this, tr("Choose Font")); if (!ok) return; spec->family = chosen.family(); spec->size = chosen.pointSizeF() > 0 ? chosen.pointSizeF() : chosen.pixelSize(); spec->bold = chosen.bold(); spec->italic = chosen.italic(); spec->underline = chosen.underline(); spec->strikeout = chosen.strikeOut(); refresh(); }); } QWidget *SettingsDialog::buildFontsTab() { auto *widget = new QWidget; auto *form = new QFormLayout(widget); m_roomFontButton = new QPushButton(widget); setupFontButton(m_roomFontButton, &m_roomFont); form->addRow(tr("Room name font:"), m_roomFontButton); m_subtitleFontButton = new QPushButton(widget); setupFontButton(m_subtitleFontButton, &m_subtitleFont); form->addRow(tr("Subtitle font:"), m_subtitleFontButton); m_objectFontButton = new QPushButton(widget); setupFontButton(m_objectFontButton, &m_objectFont); form->addRow(tr("Object list font:"), m_objectFontButton); m_lineFontButton = new QPushButton(widget); setupFontButton(m_lineFontButton, &m_lineFont); form->addRow(tr("Connection text font:"), m_lineFontButton); return widget; } QWidget *SettingsDialog::buildAdvancedTab() { auto *widget = new QWidget; auto *form = new QFormLayout(widget); m_handleSize = makeSpin(m_settings.handleSize, 64); form->addRow(tr("Handle size:"), m_handleSize); m_snapToElement = makeSpin(m_settings.snapToElementSize, 256); form->addRow(tr("Snap-to-element distance:"), m_snapToElement); m_docMargins = new QCheckBox(tr("Use document-specific margins"), widget); m_docMargins->setChecked(m_settings.documentSpecificMargins); form->addRow(QString(), m_docMargins); m_hMargin = makeSpin(m_settings.horizontalMargin, 4096); form->addRow(tr("Horizontal margin:"), m_hMargin); m_vMargin = makeSpin(m_settings.verticalMargin, 4096); form->addRow(tr("Vertical margin:"), m_vMargin); m_wrapDashes = new QCheckBox(tr("Wrap text at dashes"), widget); m_wrapDashes->setChecked(m_settings.wrapTextAtDashes); form->addRow(QString(), m_wrapDashes); return widget; } QWidget *SettingsDialog::buildRegionsTab() { auto *widget = new QWidget; auto *layout = new QVBoxLayout(widget); m_regionTable = new QTableWidget(0, 3, widget); m_regionTable->setHorizontalHeaderLabels({tr("Name"), tr("Text"), tr("Fill")}); m_regionTable->horizontalHeader()->setStretchLastSection(false); m_regionTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); auto addRow = [this](const Region &r, const QString &originalName) { const int row = m_regionTable->rowCount(); m_regionTable->insertRow(row); auto *nameItem = new QTableWidgetItem(r.name); // Remember the name this row started with, so a rename can be detected // and propagated to the rooms that use it. nameItem->setData(Qt::UserRole, originalName); m_regionTable->setItem(row, 0, nameItem); for (int col = 1; col <= 2; ++col) { auto *item = new QTableWidgetItem; item->setFlags(item->flags() & ~Qt::ItemIsEditable); const QColor c = (col == 1) ? r.textColor : r.fillColor; item->setBackground(c.isValid() ? c : QColor(Qt::white)); item->setData(Qt::UserRole, c); m_regionTable->setItem(row, col, item); } }; for (const Region &r : m_regions) addRow(r, r.name); connect(m_regionTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) { if (col == 0) return; QTableWidgetItem *item = m_regionTable->item(row, col); const QColor chosen = QColorDialog::getColor(item->data(Qt::UserRole).value(), this, tr("Choose color")); if (chosen.isValid()) { item->setBackground(chosen); item->setData(Qt::UserRole, chosen); } }); layout->addWidget(m_regionTable); auto *buttonRow = new QHBoxLayout; auto *addButton = new QPushButton(tr("Add"), widget); auto *removeButton = new QPushButton(tr("Remove"), widget); buttonRow->addWidget(addButton); buttonRow->addWidget(removeButton); buttonRow->addStretch(1); layout->addLayout(buttonRow); connect(addButton, &QPushButton::clicked, this, [this, addRow] { addRow(Region{tr("New Region"), QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}, QString()); }); connect(removeButton, &QPushButton::clicked, this, [this] { const int row = m_regionTable->currentRow(); if (row >= 0) { const QString name = m_regionTable->item(row, 0)->text(); if (name != kNoRegion) // never remove the default region m_regionTable->removeRow(row); } }); return widget; } MapSettings SettingsDialog::resultSettings() const { MapSettings s = m_settings; // keep tokens/fonts/margins we didn't expose for (int i = 0; i < ColorCount; ++i) { if (m_colorButtons[i]) s.colors[i] = buttonColor(m_colorButtons[i]); } s.gridSize = m_gridSize->value(); s.gridVisible = m_gridVisible->isChecked(); s.snapToGrid = m_snapToGrid->isChecked(); s.showOrigin = m_showOrigin->isChecked(); s.lineWidth = m_lineWidth->value(); s.arrowSize = m_arrowSize->value(); s.textOffset = m_textOffset->value(); s.defaultRoomName = m_defaultRoomName->text().isEmpty() ? QStringLiteral("Cave") : m_defaultRoomName->text(); s.defaultRoomShape = m_defaultRoomShape->currentIndex(); s.darknessStripeSize = m_darknessStripe->value(); s.objectListOffset = m_objectListOffset->value(); s.connectionStalkLength = m_stalkLength->value(); s.preferredDistanceBetweenRooms = m_preferredDistance->value(); s.roomFont = m_roomFont; s.subtitleFont = m_subtitleFont; s.objectFont = m_objectFont; s.lineFont = m_lineFont; if (m_handleSize) s.handleSize = m_handleSize->value(); if (m_snapToElement) s.snapToElementSize = m_snapToElement->value(); if (m_docMargins) s.documentSpecificMargins = m_docMargins->isChecked(); if (m_hMargin) s.horizontalMargin = m_hMargin->value(); if (m_vMargin) s.verticalMargin = m_vMargin->value(); if (m_wrapDashes) s.wrapTextAtDashes = m_wrapDashes->isChecked(); return s; } void SettingsDialog::accept() { // Validate region names: non-empty, unique, and free of the '_' and ':' // characters the exporters reserve. QSet seen; for (int row = 0; row < m_regionTable->rowCount(); ++row) { const QString name = m_regionTable->item(row, 0)->text().trimmed(); if (name.isEmpty()) { QMessageBox::warning(this, tr("Invalid Region"), tr("Region names cannot be empty.")); return; } if (name.contains(QLatin1Char('_')) || name.contains(QLatin1Char(':'))) { QMessageBox::warning(this, tr("Invalid Region"), tr("Region name '%1' may not contain '_' or ':'.").arg(name)); return; } const QString key = name.toLower(); if (seen.contains(key)) { QMessageBox::warning(this, tr("Invalid Region"), tr("Region name '%1' is used more than once.").arg(name)); return; } seen.insert(key); } QDialog::accept(); } QHash SettingsDialog::regionRenames() const { QHash renames; for (int row = 0; row < m_regionTable->rowCount(); ++row) { const QString original = m_regionTable->item(row, 0)->data(Qt::UserRole).toString(); const QString current = m_regionTable->item(row, 0)->text().trimmed(); if (!original.isEmpty() && original != current) renames.insert(original, current); } return renames; } QStringList SettingsDialog::removedRegionNames() const { QSet present; for (int row = 0; row < m_regionTable->rowCount(); ++row) { const QString original = m_regionTable->item(row, 0)->data(Qt::UserRole).toString(); if (!original.isEmpty()) present.insert(original); } QStringList removed; for (const Region &r : m_regions) { if (r.name != kNoRegion && !present.contains(r.name)) removed << r.name; } return removed; } QList SettingsDialog::resultRegions() const { QList regions; bool hasNoRegion = false; for (int row = 0; row < m_regionTable->rowCount(); ++row) { Region r; r.name = m_regionTable->item(row, 0)->text().trimmed(); if (r.name.isEmpty()) continue; if (r.name == kNoRegion) hasNoRegion = true; r.textColor = m_regionTable->item(row, 1)->data(Qt::UserRole).value(); r.fillColor = m_regionTable->item(row, 2)->data(Qt::UserRole).value(); // Preserve the exact serialized token for a region left unchanged, so // its color is not rewritten from hex to a named color and back. for (const Region &old : m_regions) { if (old.name == r.name) { if (old.textColor == r.textColor) r.textColorToken = old.textColorToken; if (old.fillColor == r.fillColor) r.fillColorToken = old.fillColorToken; break; } } regions.append(r); } if (!hasNoRegion) regions.prepend(Region{kNoRegion, QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}); return regions; } } // namespace trizbort trizbort-qt-1.0.1/src/SettingsDialog.h000066400000000000000000000117101526521612000176720ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_SETTINGSDIALOG_H #define TRIZBORT_SETTINGSDIALOG_H #include #include "MapDocument.h" class QCheckBox; class QComboBox; class QDoubleSpinBox; class QLineEdit; class QPushButton; class QTableWidget; namespace trizbort { // Editor for the document's appearance settings (palette, grid, lines, room // defaults) and its region list. result*() return the edited values. class SettingsDialog : public QDialog { Q_OBJECT public: SettingsDialog(const MapSettings &settings, const QList ®ions, QWidget *parent = nullptr); MapSettings resultSettings() const; QList resultRegions() const; // Original region name -> new name, for rows whose name changed; and the // names of original regions the user removed. Used to fix up rooms. QHash regionRenames() const; QStringList removedRegionNames() const; protected: void accept() override; // validates region names first private: QWidget *buildColorsTab(); QWidget *buildGridLinesTab(); QWidget *buildRoomsTab(); QWidget *buildFontsTab(); QWidget *buildAdvancedTab(); QWidget *buildRegionsTab(); void setButtonColor(QPushButton *button, const QColor &color); QColor buttonColor(const QPushButton *button) const; void setupFontButton(QPushButton *button, FontSpec *spec); MapSettings m_settings; QList m_regions; QPushButton *m_colorButtons[ColorCount] = {}; QDoubleSpinBox *m_gridSize = nullptr; QCheckBox *m_gridVisible = nullptr; QCheckBox *m_snapToGrid = nullptr; QCheckBox *m_showOrigin = nullptr; QDoubleSpinBox *m_lineWidth = nullptr; QDoubleSpinBox *m_arrowSize = nullptr; QDoubleSpinBox *m_textOffset = nullptr; QLineEdit *m_defaultRoomName = nullptr; QComboBox *m_defaultRoomShape = nullptr; QDoubleSpinBox *m_darknessStripe = nullptr; QDoubleSpinBox *m_objectListOffset = nullptr; QDoubleSpinBox *m_stalkLength = nullptr; QDoubleSpinBox *m_preferredDistance = nullptr; // Fonts (edited copies, applied in resultSettings). FontSpec m_roomFont; FontSpec m_subtitleFont; FontSpec m_objectFont; FontSpec m_lineFont; QPushButton *m_roomFontButton = nullptr; QPushButton *m_subtitleFontButton = nullptr; QPushButton *m_objectFontButton = nullptr; QPushButton *m_lineFontButton = nullptr; // Advanced. QDoubleSpinBox *m_handleSize = nullptr; QDoubleSpinBox *m_snapToElement = nullptr; QCheckBox *m_docMargins = nullptr; QDoubleSpinBox *m_hMargin = nullptr; QDoubleSpinBox *m_vMargin = nullptr; QCheckBox *m_wrapDashes = nullptr; QTableWidget *m_regionTable = nullptr; }; } // namespace trizbort #endif // TRIZBORT_SETTINGSDIALOG_H trizbort-qt-1.0.1/src/TranscriptAutomapper.cpp000066400000000000000000001055401526521612000215010ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "TranscriptAutomapper.h" #include #include #include #include namespace trizbort { namespace { const int kMaxCharactersBeforePrompt = 42; // Maps a movement word to a canonical direction key. Ported from // s_namesForMovementCommands. QString directionKeyForWord(const QString &word) { static const QHash kWords = { {"north", "n"}, {"n", "n"}, {"fore", "n"}, {"f", "n"}, {"south", "s"}, {"s", "s"}, {"aft", "s"}, {"a", "s"}, {"east", "e"}, {"e", "e"}, {"starboard", "e"}, {"sb", "e"}, {"west", "w"}, {"w", "w"}, {"port", "w"}, {"p", "w"}, {"northeast", "ne"}, {"ne", "ne"}, {"southeast", "se"}, {"se", "se"}, {"southwest", "sw"}, {"sw", "sw"}, {"northwest", "nw"}, {"nw", "nw"}, {"up", "u"}, {"u", "u"}, {"down", "d"}, {"d", "d"}, {"in", "in"}, {"inside", "in"}, {"out", "out"}, {"outside", "out"}, }; return kWords.value(word.toLower()); } bool isVerticalOrInsideDirection(const QString &dir) { return dir == "u" || dir == "d" || dir == "in" || dir == "out"; } QString portForDirection(const QString &dir) { if (dir == "u") return QStringLiteral("nnw"); if (dir == "d") return QStringLiteral("ssw"); if (dir == "in") return QStringLiteral("ene"); if (dir == "out") return QStringLiteral("wnw"); return dir; } void portVector(const QString &port, int &dx, int &dy) { dx = 0; dy = 0; if (port == "nnw" || port == "n" || port == "nne") { dy = -1; } else if (port == "ne") { dx = 1; dy = -1; } else if (port == "ene" || port == "e" || port == "ese") { dx = 1; } else if (port == "se") { dx = 1; dy = 1; } else if (port == "sse" || port == "s" || port == "ssw") { dy = 1; } else if (port == "sw") { dx = -1; dy = 1; } else if (port == "wsw" || port == "w" || port == "wnw") { dx = -1; } else if (port == "nw") { dx = -1; dy = -1; } } // The screen-space unit step for a direction key (Automap direction vectors: // up/down/in/out map onto north/south/east/west). void directionVector(const QString &dir, int &dx, int &dy) { portVector(portForDirection(dir), dx, dy); } QString automapOpposite(const QString &port) { static const QHash kOpposites = { {"n", "s"}, {"nne", "sse"}, {"ne", "sw"}, {"ene", "wnw"}, {"e", "w"}, {"ese", "wsw"}, {"se", "nw"}, {"sse", "nne"}, {"s", "n"}, {"ssw", "nnw"}, {"sw", "ne"}, {"wsw", "ese"}, {"w", "e"}, {"wnw", "ene"}, {"nw", "se"}, {"nnw", "ssw"}, }; return kOpposites.value(port, QStringLiteral("n")); } bool approximateDirectionMatch(const QString &portA, const QString &portB) { int ax = 0; int ay = 0; int bx = 0; int by = 0; portVector(portA, ax, ay); portVector(portB, bx, by); return ax == bx && ay == by; } void labelsForDirection(const QString &dir, bool wrongWay, QString &start, QString &end) { start.clear(); end.clear(); if (dir == "u") { start = QStringLiteral("up"); end = QStringLiteral("down"); } else if (dir == "d") { start = QStringLiteral("down"); end = QStringLiteral("up"); } else if (dir == "in") { start = QStringLiteral("in"); end = QStringLiteral("out"); } else if (dir == "out") { start = QStringLiteral("out"); end = QStringLiteral("in"); } if (wrongWay) std::swap(start, end); } void portAnchor(const QString &port, double &fx, double &fy) { fx = 0.5; fy = 0.5; if (port == "n") { fx = 0.5; fy = 0.0; } else if (port == "nne") { fx = 0.75; fy = 0.0; } else if (port == "ne") { fx = 1.0; fy = 0.0; } else if (port == "ene") { fx = 1.0; fy = 0.25; } else if (port == "e") { fx = 1.0; fy = 0.5; } else if (port == "ese") { fx = 1.0; fy = 0.75; } else if (port == "se") { fx = 1.0; fy = 1.0; } else if (port == "sse") { fx = 0.75; fy = 1.0; } else if (port == "s") { fx = 0.5; fy = 1.0; } else if (port == "ssw") { fx = 0.25; fy = 1.0; } else if (port == "sw") { fx = 0.0; fy = 1.0; } else if (port == "wsw") { fx = 0.0; fy = 0.75; } else if (port == "w") { fx = 0.0; fy = 0.5; } else if (port == "wnw") { fx = 0.0; fy = 0.25; } else if (port == "nw") { fx = 0.0; fy = 0.0; } else if (port == "nnw") { fx = 0.25; fy = 0.0; } } struct ExitVocabulary { QStringList directions; QStringList names; }; const QList &exitVocabulary() { static const QList kVocabulary = { {{"n"}, {"north", "northward", "fore", "northern"}}, {{"s"}, {"south", "southward", "aft", "southern"}}, {{"e"}, {"east", "eastward", "starboard", "eastern"}}, {{"w"}, {"west", "westward", "port", "western"}}, {{"ne"}, {"northeast", "northeastward", "northeastern"}}, {{"se"}, {"southeast", "southeastward", "southeastern"}}, {{"sw"}, {"southwest", "southwestward", "southwestern"}}, {{"nw"}, {"northwest", "northwestward", "northwestern"}}, {{"u"}, {"up", "upward", "upwards", "ascend", "above"}}, {{"d"}, {"down", "downward", "downwards", "descend", "below"}}, {{"n", "s", "e", "w", "ne", "nw", "se", "sw"}, {"all directions", "every direction"}}, }; return kVocabulary; } bool containsWholeWord(const QString &text, const QString &word) { qsizetype from = 0; while (true) { const qsizetype index = text.indexOf(word, from); if (index < 0) return false; const qsizetype end = index + word.length(); const bool startOk = index == 0 || !text.at(index - 1).isLetterOrNumber(); const bool endOk = end == text.length() || !text.at(end).isLetterOrNumber(); if (startOk && endOk) return true; from = index + 1; } } bool startsWithCapitalOrNonLetter(const QString &word) { if (word.isEmpty()) return false; const QChar c = word.at(0); if (c.isLetter() && !c.isUpper()) return false; return true; } bool isRoomDescriptionWord(const QString &word) { if (word.isEmpty()) return false; const QChar c = word.at(0); return c.isLetterOrNumber() || c == QLatin1Char('#'); } bool isAllCaps(const QString &word) { return word.toUpper() == word; } } // namespace TranscriptAutomapper::TranscriptAutomapper(const AutomapSettings &settings) : m_settings(settings) { } bool TranscriptAutomapper::isPrompt(const QString &line, QString &command) const { const qsizetype start = line.lastIndexOf(QLatin1Char('>')); if (start != -1 && start < kMaxCharactersBeforePrompt) { command = line.mid(start + 1).trimmed(); return true; } command.clear(); return false; } bool TranscriptAutomapper::extractRoomName(const QString &lineIn, const QString &previousLine, QString &name) const { name.clear(); QString unused; if (!previousLine.isNull() && previousLine.trimmed().length() > 0 && !isPrompt(previousLine, unused)) return false; // the previous line, if any, must be blank or a prompt if (lineIn.isEmpty()) return false; if (lineIn.at(0).isSpace()) return false; // lines which start with whitespace aren't room names QString line = lineIn.trimmed(); if (line.isEmpty()) return false; static const QStringList suffixMarkers = {",", "(", "[", "{", " - "}; bool stripped = true; while (stripped) { stripped = false; for (const QString &marker : suffixMarkers) { const qsizetype idx = line.indexOf(marker); if (idx >= 0) { if (line.length() - idx > 30) return false; // looks like a sentence line = line.left(idx); stripped = true; break; } } } line = line.trimmed(); if (line.isEmpty()) return false; if (!line.at(line.length() - 1).isLetterOrNumber()) return false; if (!line.at(0).isLetterOrNumber()) return false; if (!startsWithCapitalOrNonLetter(line)) return false; const QStringList words = line.split(QLatin1Char(' '), Qt::SkipEmptyParts); if (words.isEmpty()) return false; qsizetype maxWordLength = 0; int allCapsCount = 0; for (const QString &word : words) { if (!isRoomDescriptionWord(word)) return false; if (!startsWithCapitalOrNonLetter(word) && word.length() >= 4) return false; maxWordLength = qMax(maxWordLength, word.length()); if (isAllCaps(word)) ++allCapsCount; } if (words.size() > 1 && maxWordLength < 3) return false; if (allCapsCount == words.size()) return false; name = line; return true; } bool TranscriptAutomapper::extractParagraph(const QStringList &lines, int lineIndex, QString ¶graph) const { paragraph.clear(); bool have = false; while (lineIndex < lines.size()) { QString line = lines.at(lineIndex).trimmed(); if (line == QLatin1String("[Previous turn undone.]")) { ++lineIndex; continue; } if (line.isEmpty()) break; QString unused; QString unusedName; if (isPrompt(line, unused) || extractRoomName(line, lineIndex > 0 ? lines.at(lineIndex - 1) : QString(), unusedName)) break; paragraph = have ? paragraph + QLatin1Char(' ') + line : line; have = true; if (line.length() < 65) break; ++lineIndex; } return have; } QStringList TranscriptAutomapper::descriptionsFor(int roomId) const { QStringList list; if (const Room *r = m_map->roomById(roomId)) { if (!r->description.isEmpty()) list.append(r->description); } list += m_extraDescriptions.value(roomId); return list; } bool TranscriptAutomapper::matchDescription(int roomId, const QString &description) const { const QStringList list = descriptionsFor(roomId); if (description.isEmpty()) return list.isEmpty(); return list.contains(description); } void TranscriptAutomapper::addDescription(int roomId, const QString &description) { if (description.isEmpty()) return; Room *r = m_map->roomById(roomId); if (!r) return; if (descriptionsFor(roomId).contains(description)) return; if (r->description.isEmpty()) r->description = description; else m_extraDescriptions[roomId].append(description); } int TranscriptAutomapper::findRoom(const QString &name, const QString &description, const QString &line) { QList ambiguous; for (const Room &r : m_map->rooms) { if (r.name != name) continue; // A terse (non-verbose) transcript has no descriptions to tell rooms // apart, so same-named rooms are taken to be the same room without // asking (mirrors Automap.cs). if (!m_settings.verboseTranscript || m_settings.assumeSameNameSameRoom || matchDescription(r.id, description)) return r.id; ambiguous.append(r.id); } if (ambiguous.isEmpty()) return -1; if (m_dontAskAboutAmbiguities) return ambiguous.first(); if (ambiguous.size() == 1 && (description.isEmpty() || descriptionsFor(ambiguous.first()).isEmpty())) return ambiguous.first(); QList candidates; for (int id : ambiguous) { RoomCandidate c; c.id = id; c.description = descriptionsFor(id).value(0); candidates.append(c); } bool stopAsking = false; const int chosen = m_controller->disambiguateRoom(RoomQuery{name, description, line}, candidates, &stopAsking); if (stopAsking) { m_dontAskAboutAmbiguities = true; return ambiguous.first(); } return chosen; } bool TranscriptAutomapper::anyRoomIntersects(const QRectF &bounds, int exceptId) const { for (const Room &r : m_map->rooms) { if (r.id == exceptId) continue; if (QRectF(r.x, r.y, r.w, r.h).intersects(bounds)) return true; } return false; } void TranscriptAutomapper::shiftMap(const QRectF &origin, double dx, double dy) { // Push rooms on the far side of the origin away by the delta, opening space. for (Room &r : m_map->rooms) { const QRectF b(r.x, r.y, r.w, r.h); if (dx < 0) { if (b.center().x() < origin.right()) r.x += dx; } else if (dx > 0) { if (b.center().x() > origin.left()) r.x += dx; } } for (Room &r : m_map->rooms) { const QRectF b(r.x, r.y, r.w, r.h); if (dy < 0) { if (b.center().y() < origin.bottom()) r.y += dy; } else if (b.center().y() > origin.top()) { if (b.bottom() > origin.y()) r.y += dy; } } } int TranscriptAutomapper::createRoomInDirection(int existingId, const QString &dir, const QString &roomName, const QString &line) { const Room *existing = m_map->roomById(existingId); if (!existing) return -1; const double newW = 96.0; const double newH = 64.0; int dx = 0; int dy = 0; directionVector(dir, dx, dy); const double deltaX = dx * (m_settings.preferredDistanceBetweenRooms + newW); const double deltaY = dy * (m_settings.preferredDistanceBetweenRooms + newH); const double g = m_settings.gridSize > 1 ? m_settings.gridSize : 32.0; const double centerX = existing->x + existing->w / 2.0 + deltaX; const double centerY = existing->y + existing->h / 2.0 + deltaY; const double nx = std::round((centerX - newW / 2.0) / g) * g; const double ny = std::round((centerY - newH / 2.0) / g) * g; Room room; room.id = m_map->nextRoomId(); room.seq = m_map->nextSeq(); room.name = roomName; if (line != roomName) { QString subtitle = line; subtitle.replace(roomName, QString()); room.subtitle = subtitle; } room.w = newW; room.h = newH; room.x = nx; room.y = ny; if (anyRoomIntersects(QRectF(room.x, room.y, room.w, room.h), room.id)) shiftMap(QRectF(room.x, room.y, room.w, room.h), deltaX, deltaY); m_map->rooms.append(room); m_map->reindex(); ++m_roomsAdded; return room.id; } int TranscriptAutomapper::roomConnectedInDirection(int fromId, const QString &dir) const { const QString port = portForDirection(dir); for (const Connection &c : m_map->connections) { if (c.vertices.size() < 2) continue; for (int i = 0; i < 2; ++i) { const Vertex &v = c.vertices.at(i); if (!v.docked || v.roomId != fromId || v.port != port) continue; const Vertex &other = c.vertices.at(1 - i); if (other.docked && other.roomId != fromId) return other.roomId; } } return -1; } bool TranscriptAutomapper::connectionDockedAt(int roomId, const QString &port) const { for (const Connection &c : m_map->connections) { for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId && v.port == port) return true; } } return false; } void TranscriptAutomapper::removeRoomById(int roomId) { m_map->removeRoom(roomId); // also drops connections docked to it m_map->reindex(); m_extraDescriptions.remove(roomId); } void TranscriptAutomapper::addExitStub(int roomId, const QString &dir) { const Room *r = m_map->roomById(roomId); if (!r) return; const QString port = portForDirection(dir); int dx = 0; int dy = 0; portVector(port, dx, dy); if (dx == 0 && dy == 0) return; if (connectionDockedAt(roomId, port)) return; double fx = 0.5; double fy = 0.5; portAnchor(port, fx, fy); const double px = r->x + r->w * fx; const double py = r->y + r->h * fy; const double stub = m_settings.preferredDistanceBetweenRooms / 2.0 + 16.0; Connection c; c.id = m_map->nextConnectionId(); c.seq = m_map->nextSeq(); if (dir == "u") c.startText = QStringLiteral("up"); else if (dir == "d") c.startText = QStringLiteral("down"); Vertex a; a.index = 0; a.docked = true; a.roomId = roomId; a.port = port; Vertex b; b.index = 1; b.docked = false; b.point = QPointF(px + dx * stub, py + dy * stub); c.vertices << a << b; m_map->connections.append(c); ++m_connectionsAdded; } void TranscriptAutomapper::removeExitStub(int roomId, const QString &dir) { const QString port = portForDirection(dir); int dx = 0; int dy = 0; portVector(port, dx, dy); if (dx == 0 && dy == 0) return; for (qsizetype i = m_map->connections.size() - 1; i >= 0; --i) { const Connection &c = m_map->connections.at(i); bool dockHere = false; bool hasFree = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId && v.port == port) dockHere = true; if (!v.docked) hasFree = true; } if (dockHere && hasFree) m_map->connections.removeAt(i); } } void TranscriptAutomapper::guessExitsFromDescription(int roomId, const QString &description) { if (!m_settings.guessExits || description.isEmpty()) return; const QString lower = description.toLower(); for (const ExitVocabulary &entry : exitVocabulary()) { for (const QString &name : entry.names) { if (!containsWholeWord(lower, name)) continue; for (const QString &dir : entry.directions) addExitStub(roomId, dir); } } } int TranscriptAutomapper::createRoomTeleport(const QString &name) { const double newW = 96.0; const double newH = 64.0; const double g = m_settings.gridSize > 1 ? m_settings.gridSize : 32.0; const Room *existing = m_map->roomById(m_lastRoomId); double baseX = existing ? existing->x : 0.0; double baseY = existing ? existing->y : 0.0; baseX = std::round(baseX / g) * g; baseY = std::round(baseY / g) * g; Room room; room.id = m_map->nextRoomId(); room.seq = m_map->nextSeq(); room.name = name; room.w = newW; room.h = newH; room.x = baseX; room.y = baseY; // Alternate left then right, expanding, until a blank space is found. bool tryOtherSideNext = false; bool tryLeft = true; int distance = 0; const double initialX = room.x; while (anyRoomIntersects(QRectF(room.x, room.y, room.w, room.h), room.id)) { if (tryOtherSideNext) { tryLeft = !tryLeft; tryOtherSideNext = false; } else { tryOtherSideNext = true; ++distance; } const double f = distance * (m_settings.preferredDistanceBetweenRooms + room.w); const double vx = tryLeft ? initialX - f : initialX + f; room.x = std::round(vx / g) * g; } m_map->rooms.append(room); m_map->reindex(); ++m_roomsAdded; return room.id; } int TranscriptAutomapper::findConnection(int sourceId, int targetId, const QString &acceptablePort, bool &wrongWay) { wrongWay = false; for (qsizetype i = 0; i < m_map->connections.size(); ++i) { Connection &c = m_map->connections[i]; if (c.vertices.size() < 2 || !c.vertices.at(0).docked || !c.vertices.at(1).docked) continue; Vertex &from = c.vertices[0]; Vertex &to = c.vertices[1]; if (from.roomId == sourceId && to.roomId == targetId && (acceptablePort.isEmpty() || approximateDirectionMatch(acceptablePort, from.port))) { wrongWay = false; return static_cast(i); } if (from.roomId == targetId && to.roomId == sourceId && (acceptablePort.isEmpty() || approximateDirectionMatch(acceptablePort, to.port))) { wrongWay = c.flow == ConnectionFlow::OneWay; return static_cast(i); } if (from.roomId == targetId && to.roomId == sourceId) { if (!acceptablePort.isEmpty()) to.port = acceptablePort; wrongWay = c.flow == ConnectionFlow::OneWay; return static_cast(i); } if (from.roomId == sourceId && to.roomId == targetId) { if (!acceptablePort.isEmpty()) from.port = acceptablePort; wrongWay = false; return static_cast(i); } } return -1; } void TranscriptAutomapper::connectRooms(int sourceId, const QString &dir, int targetId) { if (!m_map->roomById(sourceId) || !m_map->roomById(targetId) || sourceId == targetId) return; // A real move supersedes any exit stub that pointed this way. removeExitStub(sourceId, dir); const QString srcPort = portForDirection(dir); int dx = 0; int dy = 0; portVector(srcPort, dx, dy); if (dx == 0 && dy == 0) return; const bool vertical = isVerticalOrInsideDirection(dir); bool wrongWay = false; int index = findConnection(sourceId, targetId, vertical ? QString() : srcPort, wrongWay); if (index < 0) { Connection conn; conn.id = m_map->nextConnectionId(); conn.seq = m_map->nextSeq(); conn.flow = m_settings.assumeTwoWayConnections ? ConnectionFlow::TwoWay : ConnectionFlow::OneWay; conn.style = m_useDottedConnection ? ConnectionStyle::Dashed : ConnectionStyle::Solid; m_useDottedConnection = false; Vertex v0; v0.index = 0; v0.docked = true; v0.roomId = sourceId; v0.port = srcPort; Vertex v1; v1.index = 1; v1.docked = true; v1.roomId = targetId; v1.port = automapOpposite(srcPort); conn.vertices << v0 << v1; m_map->connections.append(conn); ++m_connectionsAdded; index = static_cast(m_map->connections.size()) - 1; } else if (wrongWay) { m_map->connections[index].flow = ConnectionFlow::TwoWay; } Connection &conn = m_map->connections[index]; if (vertical && conn.startText.isEmpty() && conn.endText.isEmpty()) labelsForDirection(dir, wrongWay, conn.startText, conn.endText); } void TranscriptAutomapper::processTranscriptText(const QStringList &lines) { QString previousLine; // null for (int index = 0; index < lines.size(); ++index) { const QString &line = lines.at(index); QString roomName; if (extractRoomName(line, previousLine, roomName)) { QString roomDescription; extractParagraph(lines, index + 1, roomDescription); int roomId = findRoom(roomName, roomDescription, line); if (roomId < 0) { // new room if (m_lastRoomId >= 0 && !m_lastMoveDir.isEmpty()) { const int otherRoom = roomConnectedInDirection(m_lastRoomId, m_lastMoveDir); if (otherRoom >= 0) { const AutomapController::SameDir choice = m_controller->sameDirection( m_lastRoomId, otherRoom, m_lastMoveDir, roomName); switch (choice) { case AutomapController::SameDir::KeepExisting: break; case AutomapController::SameDir::KeepNew: roomId = createRoomInDirection(m_lastRoomId, m_lastMoveDir, roomName, line); connectRooms(m_lastRoomId, m_lastMoveDir, roomId); removeRoomById(otherRoom); break; case AutomapController::SameDir::KeepBoth: roomId = createRoomInDirection(m_lastRoomId, m_lastMoveDir, roomName, line); connectRooms(m_lastRoomId, m_lastMoveDir, roomId); break; } } else { roomId = createRoomInDirection(m_lastRoomId, m_lastMoveDir, roomName, line); connectRooms(m_lastRoomId, m_lastMoveDir, roomId); } } else { if (m_firstRoom || m_gameName == roomName) { // most likely the game title m_firstRoom = false; m_gameName = roomName; roomId = -1; } else { // teleported to a new room; don't connect it up const bool wasFirst = (m_lastRoomId < 0); roomId = createRoomTeleport(roomName); if (wasFirst) { if (Room *r = m_map->roomById(roomId)) r->isStartRoom = true; } } } if (roomId >= 0) { guessExitsFromDescription(roomId, roomDescription); m_lastRoomId = roomId; } } else if (roomId != m_lastRoomId) { // moved to an existing room if (m_lastRoomId >= 0 && !m_lastMoveDir.isEmpty()) connectRooms(m_lastRoomId, m_lastMoveDir, roomId); m_lastRoomId = roomId; } // else: still in the same room if (roomId >= 0) addDescription(roomId, roomDescription); m_lastMoveDir.clear(); } previousLine = line; } } void TranscriptAutomapper::processPromptCommand(const QString &commandIn) { m_lastMoveDir.clear(); const QString command = commandIn; // (default "tb region") if (command.startsWith(m_settings.regionCommand, Qt::CaseInsensitive)) { const QString regionName = command.mid(m_settings.regionCommand.length()).trimmed(); if (!regionName.isEmpty() && m_lastRoomId >= 0) { if (!m_map->regionByName(regionName)) m_map->regions.append( Region{regionName, QColor(25, 25, 112), QColor(255, 255, 255), QString(), QString()}); if (Room *r = m_map->roomById(m_lastRoomId)) r->region = regionName; } return; } // (default "tb see") if (command.startsWith(m_settings.objectCommand, Qt::CaseInsensitive)) { const QString objectName = command.mid(m_settings.objectCommand.length()).trimmed(); if (!objectName.isEmpty() && m_lastRoomId >= 0) { if (Room *r = m_map->roomById(m_lastRoomId)) { QString text = r->objectsText; text.remove(QLatin1Char('\r')); const QStringList existing = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts); bool found = false; for (const QString &o : existing) { if (o.trimmed().compare(objectName, Qt::CaseInsensitive) == 0) { found = true; break; } } if (!found) r->objectsText = r->objectsText.isEmpty() ? objectName : r->objectsText + QStringLiteral("\r\n") + objectName; } } return; } QStringList parts = command.split(QLatin1Char(' '), Qt::SkipEmptyParts); if (parts.isEmpty()) return; static const QStringList strip = {"the", "a", "to", "on"}; QStringList words; for (const QString &word : parts) { bool skip = false; for (const QString &s : strip) { if (word.compare(s, Qt::CaseInsensitive) == 0) { skip = true; break; } } if (!word.isEmpty() && word.at(0) == QLatin1Char('[')) { // strip bracketed turn counters like "[12]" for (int i = 1; i < word.length(); ++i) { if (word.at(i) == QLatin1Char(']')) { skip = true; break; } if (!word.at(i).isDigit()) break; } } if (!skip) words.append(word); } static const QStringList goWords = {"go", "walk", "move"}; if (!words.isEmpty()) { for (const QString &g : goWords) { if (words.first().compare(g, Qt::CaseInsensitive) == 0) { words.removeFirst(); break; } } } // trypush : nudge the current room one step in a direction. if (!words.isEmpty() && words.first().compare(QLatin1String("trypush"), Qt::CaseInsensitive) == 0) { if (words.size() > 1 && m_lastRoomId >= 0) { const QString d = directionKeyForWord(words.at(1)); int dx = 0; int dy = 0; directionVector(d, dx, dy); if ((dx != 0 || dy != 0)) { if (Room *r = m_map->roomById(m_lastRoomId)) { const double gap = m_settings.preferredDistanceBetweenRooms; r->x += dx * (r->w + gap); r->y += dy * (r->h + gap); } } } return; } // tb dotted / tb exit / tb noexit if (!words.isEmpty() && words.first().compare(QLatin1String("tb"), Qt::CaseInsensitive) == 0) { if (words.size() > 1) { const QString sub = words.at(1).toLower(); if (sub == QLatin1String("dotted")) { m_useDottedConnection = true; } else if (sub == QLatin1String("exit") && words.size() > 2 && m_lastRoomId >= 0) { const QString d = directionKeyForWord(words.at(2)); if (!d.isEmpty()) addExitStub(m_lastRoomId, d); } else if (sub == QLatin1String("noexit") && words.size() > 2 && m_lastRoomId >= 0) { const QString d = directionKeyForWord(words.at(2)); if (!d.isEmpty()) removeExitStub(m_lastRoomId, d); } } return; } if (words.size() != 1) return; const QString dir = directionKeyForWord(words.first()); if (!dir.isEmpty()) { m_lastMoveDir = dir; if (m_lastRoomId >= 0) removeExitStub(m_lastRoomId, dir); } } namespace { // The non-interactive default controller used by batch run(). AutomapController &defaultController() { static AutomapController controller; return controller; } } // namespace void TranscriptAutomapper::begin(Map &map, AutomapController *controller) { m_map = ↦ m_controller = controller ? controller : &defaultController(); m_lastRoomId = -1; m_lastMoveDir.clear(); m_firstRoom = true; m_gameName.clear(); m_useDottedConnection = false; m_dontAskAboutAmbiguities = false; m_extraDescriptions.clear(); m_buffer.clear(); m_pos = 0; m_between.clear(); m_canceled = false; m_roomsAdded = 0; m_connectionsAdded = 0; } bool TranscriptAutomapper::feed(const QStringList &lines, bool final) { m_buffer += lines; processBuffer(final); if (m_map) m_map->reindex(); return !m_canceled; } void TranscriptAutomapper::processBuffer(bool final) { while (m_pos < m_buffer.size()) { if (m_canceled) return; const QString line = m_buffer.at(m_pos); QString command; if (isPrompt(line, command)) { // Gate for single-stepping before each command. if (m_controller && !m_controller->step()) { m_canceled = true; return; } processTranscriptText(m_between); m_between.clear(); processPromptCommand(command); } else { m_between.append(line); } ++m_pos; } if (final) { // The last room in a saved transcript often has no following prompt. if (!m_between.isEmpty() && m_controller && !m_controller->step()) { m_canceled = true; return; } processTranscriptText(m_between); m_between.clear(); } } int TranscriptAutomapper::run(Map &map, const QStringList &lines) { begin(map, nullptr); feed(lines, true); return m_roomsAdded; } int TranscriptAutomapper::run(Map &map, const QString &text) { QString normalized = text; normalized.replace(QLatin1String("\r\n"), QLatin1String("\n")); normalized.replace(QLatin1Char('\r'), QLatin1Char('\n')); return run(map, normalized.split(QLatin1Char('\n'))); } } // namespace trizbort trizbort-qt-1.0.1/src/TranscriptAutomapper.h000066400000000000000000000220321526521612000211400ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_TRANSCRIPTAUTOMAPPER_H #define TRIZBORT_TRANSCRIPTAUTOMAPPER_H #include #include #include #include #include #include "MapDocument.h" // Transcript-driven automapper, ported from Automap/Automap.cs (the parser) and // UI/Controls/Canvas.Automap.cs (the layout). Reads a play-session transcript // and builds rooms and connections into a Map, placing new rooms a grid step // away in the movement direction and shifting the map aside to avoid overlaps. namespace trizbort { struct AutomapSettings { bool assumeTwoWayConnections = true; // When true, rooms sharing a name are taken to be the same room regardless // of description; when false (the C# default) the controller is asked to // disambiguate whenever the descriptions differ. bool assumeSameNameSameRoom = false; // When false the transcript is treated as terse (no room descriptions), so // rooms sharing a name are always taken to be the same room without asking // (Automap.cs: "must assume room with same name is same room" when not // verbose). When true, disambiguation applies as normal. bool verboseTranscript = true; // Live automap only: skip everything already in the transcript file and // start from its end, so only newly-played moves are mapped (C# // ContinueTranscript). bool continueTranscript = false; // Add dangling exit stubs for direction words found in a room's description. bool guessExits = true; double preferredDistanceBetweenRooms = 64.0; double gridSize = 32.0; // Customizable command words (the C# AutomapDialog options). QString objectCommand = QStringLiteral("tb see"); QString regionCommand = QStringLiteral("tb region"); }; // Callbacks the automapper uses for interactive decisions and progress. The // default (base-class) behavior is non-interactive, matching a batch import; // the GUI supplies a subclass that shows dialogs and gates single-stepping. // This mirrors the C# IAutomapCanvas / Automap interaction points. struct RoomQuery { QString name; QString description; QString line; }; struct RoomCandidate { int id = -1; QString description; }; class AutomapController { public: virtual ~AutomapController() = default; enum class SameDir { KeepExisting, KeepNew, KeepBoth }; // A room name matched existing rooms with other descriptions: return the // chosen room id, or -1 to create a new room; set *stopAsking to auto-choose // from now on. Default: reuse the first candidate. virtual int disambiguateRoom(const RoomQuery &query, const QList &candidates, bool *stopAsking) { Q_UNUSED(query); Q_UNUSED(stopAsking); return candidates.isEmpty() ? -1 : candidates.first().id; } // Moving `dir` from a room, a connection that way already leads to a // different room. Default: keep both (place the new room and shift to make room). virtual SameDir sameDirection(int fromRoomId, int existingRoomId, const QString &dir, const QString &newRoomName) { Q_UNUSED(fromRoomId); Q_UNUSED(existingRoomId); Q_UNUSED(dir); Q_UNUSED(newRoomName); return SameDir::KeepBoth; } // Gate invoked before each transcript command; return false to cancel. virtual bool step() { return true; } virtual void status(const QString &message) { Q_UNUSED(message); } }; class TranscriptAutomapper { public: explicit TranscriptAutomapper(const AutomapSettings &settings = {}); // Apply a whole transcript to the map in one shot (batch). Returns the // number of rooms added. Uses the non-interactive default controller. int run(Map &map, const QStringList &lines); int run(Map &map, const QString &text); // Incremental, controller-driven use (live automap / stepping): // begin() binds the map and controller and resets state; // feed() appends transcript lines and processes complete commands // (call with final=true for the last chunk); returns false if canceled. void begin(Map &map, AutomapController *controller); bool feed(const QStringList &lines, bool final); int roomsAdded() const { return m_roomsAdded; } int connectionsAdded() const { return m_connectionsAdded; } private: // Transcript parsing (Automap.cs). bool isPrompt(const QString &line, QString &command) const; bool extractRoomName(const QString &line, const QString &previousLine, QString &name) const; bool extractParagraph(const QStringList &lines, int lineIndex, QString ¶graph) const; void processTranscriptText(const QStringList &lines); void processPromptCommand(const QString &command); // Canvas operations on the map (Canvas.Automap.cs). int findRoom(const QString &name, const QString &description, const QString &line); int createRoomInDirection(int existingId, const QString &dir, const QString &roomName, const QString &line); int createRoomTeleport(const QString &name); void connectRooms(int sourceId, const QString &dir, int targetId); int findConnection(int sourceId, int targetId, const QString &acceptablePort, bool &wrongWay); bool anyRoomIntersects(const QRectF &bounds, int exceptId) const; void shiftMap(const QRectF &origin, double dx, double dy); int roomConnectedInDirection(int fromId, const QString &dir) const; bool connectionDockedAt(int roomId, const QString &port) const; void removeRoomById(int roomId); // Exit stubs: a dangling connection out of a room in a direction. void addExitStub(int roomId, const QString &dir); void removeExitStub(int roomId, const QString &dir); void guessExitsFromDescription(int roomId, const QString &description); QStringList descriptionsFor(int roomId) const; bool matchDescription(int roomId, const QString &description) const; void addDescription(int roomId, const QString &description); // Process one buffered transcript line (prompt or accumulating room text). void processBuffer(bool final); AutomapSettings m_settings; Map *m_map = nullptr; AutomapController *m_controller = nullptr; int m_lastRoomId = -1; QString m_lastMoveDir; // empty => none bool m_firstRoom = true; QString m_gameName; bool m_useDottedConnection = false; bool m_dontAskAboutAmbiguities = false; QHash m_extraDescriptions; // Incremental parsing state. QStringList m_buffer; // all transcript lines fed so far int m_pos = 0; // next unprocessed line in m_buffer QStringList m_between; // lines accumulated since the last prompt bool m_canceled = false; int m_roomsAdded = 0; int m_connectionsAdded = 0; }; } // namespace trizbort #endif // TRIZBORT_TRANSCRIPTAUTOMAPPER_H trizbort-qt-1.0.1/src/TrizbortReader.cpp000066400000000000000000000625651526521612000202650ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "TrizbortReader.h" #include #include #include #include #include namespace trizbort { namespace { QColor colorFromArgbInt(qint64 value) { const unsigned int argb = static_cast(value & 0xFFFFFFFFu); const int alpha = static_cast((argb >> 24) & 0xFF); if (alpha == 0) return QColor(); return QColor(static_cast((argb >> 16) & 0xFF), static_cast((argb >> 8) & 0xFF), static_cast(argb & 0xFF), alpha); } QColor colorFromDotNetConverterForm(const QString &t) { if (t.contains(QLatin1Char(','))) { const QStringList parts = t.split(QLatin1Char(',')); if (parts.size() != 3 && parts.size() != 4) return QColor(); QList values; for (const QString &p : parts) { bool ok = false; const int v = p.trimmed().toInt(&ok); if (!ok || v < 0 || v > 255) return QColor(); values.append(v); } if (values.size() == 3) return QColor(values.at(0), values.at(1), values.at(2)); if (values.at(0) == 0) return QColor(); return QColor(values.at(1), values.at(2), values.at(3), values.at(0)); } bool ok = false; if (t.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) { const qint64 v = t.mid(2).toLongLong(&ok, 16); return ok ? colorFromArgbInt(v) : QColor(); } const qint64 v = t.toLongLong(&ok, 10); return ok ? colorFromArgbInt(v) : QColor(); } } // namespace QColor parseTrizbortColor(const QString &text) { const QString t = text.trimmed(); if (t.isEmpty()) return QColor(); // invalid => inherit default #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) const QColor named = QColor::fromString(t); #else QColor named; named.setNamedColor(t); #endif if (named.isValid()) return named; return colorFromDotNetConverterForm(t); } namespace { bool nameIs(QStringView name, const char *expected) { return name.compare(QLatin1String(expected), Qt::CaseInsensitive) == 0; } bool hasAttr(const QXmlStreamAttributes &a, const char *name) { for (const QXmlStreamAttribute &x : a) if (nameIs(x.name(), name)) return true; return false; } QString attr(const QXmlStreamAttributes &a, const char *name) { for (const QXmlStreamAttribute &x : a) if (nameIs(x.name(), name)) return x.value().toString(); return QString(); } // Trizbort persists booleans as "yes"/"no" (XmlScribe); tolerate "true"/"1" too. bool toBool(const QString &text, bool defaultValue = false) { const QString t = text.trimmed().toLower(); if (t == QLatin1String("yes") || t == QLatin1String("true") || t == QLatin1String("1")) return true; if (t == QLatin1String("no") || t == QLatin1String("false") || t == QLatin1String("0")) return false; return defaultValue; } double toDouble(const QString &text, double defaultValue = 0.0) { bool ok = false; QString t = text.trimmed(); t.remove(QLatin1Char(',')); const double v = t.toDouble(&ok); return ok ? v : defaultValue; } double clampFontSize(double size) { return std::clamp(size, kMinFontSize, kMaxFontSize); } QString normalizeModifierKeys(const QString &text, const QString &defaultValue) { if (text.trimmed().isEmpty()) return defaultValue; bool shift = false; bool control = false; bool alt = false; for (const QString &part : text.split(QLatin1Char('|'))) { const QString p = part.trimmed().toLower(); if (p == QLatin1String("shift")) shift = true; else if (p == QLatin1String("control")) control = true; else if (p == QLatin1String("alt")) alt = true; } QStringList parts; if (shift) parts << QStringLiteral("shift"); if (control) parts << QStringLiteral("control"); if (alt) parts << QStringLiteral("alt"); if (parts.isEmpty()) return QStringLiteral("none"); return parts.join(QLatin1Char('|')); } void readObjects(QXmlStreamReader &xml, Room &r) { const QXmlStreamAttributes a = xml.attributes(); r.objectsPosition.at = attr(a, "at"); r.objectsPosition.custom = toBool(attr(a, "custom")); r.objectsPosition.customRight = attr(a, "customRight").toInt(); r.objectsPosition.customDown = attr(a, "customDown").toInt(); // Trizbort encodes newlines in the objects text as '|' (and an escaped // literal pipe as '\|'); decode exactly as Room.cs:1162 does. Indentation // denotes containment and [..] encodes properties; the exporters parse that. QString text = xml.readElementText(); text.replace(QLatin1String("|"), QLatin1String("\r\n")); text.replace(QLatin1String("\\\r\n"), QLatin1String("|")); r.objectsText = text; } // Files older than 1.5.8.3 stored roomFill="#FFFFFF" to mean "no fill"; such a // room's colors are all ignored on load, exactly as Room.cs does for that // version range. bool isPreColorFixVersion(const QString &version) { if (version.trimmed().isEmpty()) return true; const QStringList parts = version.split(QLatin1Char('.')); const int target[4] = {1, 5, 8, 3}; for (int i = 0; i < 4; ++i) { const int v = i < parts.size() ? parts.at(i).toInt() : 0; if (v != target[i]) return v < target[i]; } return false; // exactly 1.5.8.3 => already fixed } Room readRoom(QXmlStreamReader &xml, bool legacyWhiteFillQuirk) { const QXmlStreamAttributes a = xml.attributes(); Room r; r.id = attr(a, "id").toInt(); r.name = attr(a, "name"); r.subtitle = attr(a, "subtitle"); r.description = attr(a, "description"); r.region = normalizeRegionName(attr(a, "region")); if (hasAttr(a, "referenceRoom")) r.referenceRoom = attr(a, "referenceRoom").toInt(); r.x = toDouble(attr(a, "x")); r.y = toDouble(attr(a, "y")); if (hasAttr(a, "w")) r.w = toDouble(attr(a, "w"), r.w); if (hasAttr(a, "h")) r.h = toDouble(attr(a, "h"), r.h); r.handDrawn = toBool(attr(a, "handDrawn")); r.allCornersEqual = toBool(attr(a, "allcornersequal")); if (toBool(attr(a, "roundedCorners"))) r.roundedCorners = true; else if (toBool(attr(a, "octagonal"))) r.octagonal = true; else if (toBool(attr(a, "ellipse"))) r.ellipse = true; r.cornerTopLeft = clampCornerRadius(toDouble(attr(a, "cornerTopLeft"), r.cornerTopLeft)); r.cornerTopRight = clampCornerRadius(toDouble(attr(a, "cornerTopRight"), r.cornerTopRight)); r.cornerBottomLeft = clampCornerRadius(toDouble(attr(a, "cornerBottomLeft"), r.cornerBottomLeft)); r.cornerBottomRight = clampCornerRadius(toDouble(attr(a, "cornerBottomRight"), r.cornerBottomRight)); const QString bs = attr(a, "borderstyle"); if (!bs.isEmpty()) r.borderStyle = bs; r.isDark = toBool(attr(a, "isDark")); r.isStartRoom = toBool(attr(a, "isStartRoom")); r.isEndRoom = toBool(attr(a, "isEndRoom")); // For pre-1.5.8.3 files a white roomFill means "no colors": leave every // room color at its inherited default (Room.cs:1145-1151). if (!(legacyWhiteFillQuirk && attr(a, "roomFill") == QLatin1String("#FFFFFF"))) { r.fill = parseTrizbortColor(attr(a, "roomFill")); r.secondFill = parseTrizbortColor(attr(a, "secondFill")); const QString loc = attr(a, "secondFillLocation"); if (!loc.isEmpty()) r.secondFillLocation = loc; r.border = parseTrizbortColor(attr(a, "roomBorder")); r.largeText = parseTrizbortColor(attr(a, "roomLargeText")); r.subtitleColor = parseTrizbortColor(attr(a, "roomSubtitleColor")); r.smallText = parseTrizbortColor(attr(a, "roomSmallText")); } if (hasAttr(a, "ZOrder")) r.zOrder = attr(a, "ZOrder").toInt(); while (xml.readNextStartElement()) { if (nameIs(xml.name(), "objects")) readObjects(xml, r); else xml.skipCurrentElement(); } return r; } Connection readLine(QXmlStreamReader &xml) { const QXmlStreamAttributes a = xml.attributes(); Connection c; c.id = attr(a, "id").toInt(); c.style = (attr(a, "style") == QLatin1String("dashed")) ? ConnectionStyle::Dashed : ConnectionStyle::Solid; c.flow = (attr(a, "flow") == QLatin1String("oneWay")) ? ConnectionFlow::OneWay : ConnectionFlow::TwoWay; c.name = attr(a, "name"); c.description = attr(a, "description"); c.startText = attr(a, "startText"); c.midText = attr(a, "midText"); c.endText = attr(a, "endText"); c.color = parseTrizbortColor(attr(a, "color")); if (toBool(attr(a, "door"))) { c.hasDoor = true; c.door.lockable = toBool(attr(a, "lockable")); c.door.openable = toBool(attr(a, "openable")); c.door.locked = toBool(attr(a, "locked")); c.door.open = toBool(attr(a, "open")); } while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "dock")) { const QXmlStreamAttributes va = xml.attributes(); Vertex v; v.index = attr(va, "index").toInt(); v.docked = true; v.roomId = attr(va, "id").toInt(); v.port = attr(va, "port"); c.vertices.append(v); xml.skipCurrentElement(); } else if (nameIs(n, "point")) { const QXmlStreamAttributes va = xml.attributes(); Vertex v; v.index = attr(va, "index").toInt(); v.docked = false; v.point = QPointF(toDouble(attr(va, "x")), toDouble(attr(va, "y"))); c.vertices.append(v); xml.skipCurrentElement(); } else { xml.skipCurrentElement(); } } std::stable_sort(c.vertices.begin(), c.vertices.end(), [](const Vertex &lhs, const Vertex &rhs) { return lhs.index < rhs.index; }); for (int i = 0; i < c.vertices.size(); ++i) c.vertices[i].index = i; return c; } void readInfo(QXmlStreamReader &xml, Map &out) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "title")) out.title = xml.readElementText(); else if (nameIs(n, "author")) out.author = xml.readElementText(); else if (nameIs(n, "description")) out.description = xml.readElementText(); else if (nameIs(n, "history")) out.history = xml.readElementText(); else xml.skipCurrentElement(); } } void readMap(QXmlStreamReader &xml, Map &out) { int seq = 0; const bool legacyWhiteFill = isPreColorFixVersion(out.version); QSet usedIds; int maxId = 0; auto assignId = [&usedIds, &maxId](const QXmlStreamAttributes &a) { bool ok = false; int id = attr(a, "id").trimmed().toInt(&ok); if (!ok || usedIds.contains(id)) id = maxId + 1; usedIds.insert(id); maxId = qMax(maxId, id); return id; }; while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "room")) { // Note whether this room states handDrawn explicitly, so the legacy // map-wide flag is only applied to files that predate per-room values. if (hasAttr(xml.attributes(), "handDrawn")) out.settings.anyRoomHandDrawnExplicit = true; const int id = assignId(xml.attributes()); Room r = readRoom(xml, legacyWhiteFill); r.id = id; r.seq = ++seq; out.rooms.append(r); } else if (nameIs(n, "line")) { const int id = assignId(xml.attributes()); Connection c = readLine(xml); c.id = id; c.seq = ++seq; out.connections.append(c); } else { xml.skipCurrentElement(); } } } // Maps a child element name to its palette slot (Colors.Names in C#). // Names not in this table (e.g. legacy "fill"/"largeText") are ignored, exactly // as Colors.FromName does. int colorSlotFromName(const QStringView name) { static const struct { QLatin1String tag; ColorSlot slot; } kSlots[] = { {QLatin1String("canvas"), ColorCanvas}, {QLatin1String("border"), ColorBorder}, {QLatin1String("line"), ColorLine}, {QLatin1String("selectedLine"), ColorSelectedLine}, {QLatin1String("hoverLine"), ColorHoverLine}, {QLatin1String("subTitle"), ColorSubtitle}, {QLatin1String("smallText"), ColorSmallText}, {QLatin1String("lineText"), ColorLineText}, {QLatin1String("grid"), ColorGrid}, {QLatin1String("startRoom"), ColorStartRoom}, {QLatin1String("endRoom"), ColorEndRoom}, }; for (const auto &s : kSlots) { if (name.compare(s.tag, Qt::CaseInsensitive) == 0) return s.slot; } return -1; } void readColors(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const int slot = colorSlotFromName(xml.name()); const QString token = xml.readElementText(); const QColor c = parseTrizbortColor(token); if (slot >= 0 && c.isValid()) { s.colors[slot] = c; s.colorTokens[slot] = token; // preserve exact serialized form } } } void readRegions(QXmlStreamReader &xml, Map &out) { while (xml.readNextStartElement()) { Region reg; const QXmlStreamAttributes a = xml.attributes(); QString name = attr(a, "Name"); if (name.isEmpty()) name = xml.name().toString(); // fall back to the element tag reg.name = name.replace(QLatin1String("____"), QLatin1String(" ")); // ClearRegionNameObfuscation const QString tc = attr(a, "TextColor"); reg.textColor = tc.isEmpty() ? QColor(0, 0, 255) : parseTrizbortColor(tc); // default Blue reg.textColorToken = tc; const QString fillToken = xml.readElementText(); // consumes element const QColor fill = parseTrizbortColor(fillToken); reg.fillColor = fill.isValid() ? fill : QColor(255, 255, 255); // default White reg.fillColorToken = fillToken; out.regions.append(reg); } } void readFontSpec(QXmlStreamReader &xml, FontSpec &f) { const QXmlStreamAttributes a = xml.attributes(); f.size = clampFontSize(toDouble(attr(a, "size"), f.size)); f.bold = toBool(attr(a, "bold")); f.italic = toBool(attr(a, "italic")); f.underline = toBool(attr(a, "underline")); f.strikeout = toBool(attr(a, "strikeout")); const QString family = xml.readElementText(); if (!family.isEmpty()) f.family = family; } void readFonts(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "room")) readFontSpec(xml, s.roomFont); else if (nameIs(n, "object")) readFontSpec(xml, s.objectFont); else if (nameIs(n, "subTitle")) readFontSpec(xml, s.subtitleFont); else if (nameIs(n, "line")) readFontSpec(xml, s.lineFont); else xml.skipCurrentElement(); } } void readGrid(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "snapTo")) s.snapToGrid = toBool(xml.readElementText(), s.snapToGrid); else if (nameIs(n, "visible")) s.gridVisible = toBool(xml.readElementText(), s.gridVisible); else if (nameIs(n, "showOrigin")) s.showOrigin = toBool(xml.readElementText(), s.showOrigin); else if (nameIs(n, "size")) s.gridSize = toDouble(xml.readElementText(), s.gridSize); else xml.skipCurrentElement(); } } void readLines(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "width")) s.lineWidth = toDouble(xml.readElementText(), s.lineWidth); else if (nameIs(n, "arrowSize")) s.arrowSize = toDouble(xml.readElementText(), s.arrowSize); else if (nameIs(n, "textOffset")) s.textOffset = toDouble(xml.readElementText(), s.textOffset); else if (nameIs(n, "handDrawn")) // Legacy map-wide hand-drawn flag; applied to rooms after load. s.handDrawnGlobal = toBool(xml.readElementText(), s.handDrawnGlobal); else xml.skipCurrentElement(); } } void readRooms(QXmlStreamReader &xml, MapSettings &s) { bool sawPreferredDistance = false; while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "darknessStripeSize")) s.darknessStripeSize = toDouble(xml.readElementText(), s.darknessStripeSize); else if (nameIs(n, "objectListOffset")) s.objectListOffset = toDouble(xml.readElementText(), s.objectListOffset); else if (nameIs(n, "connectionStalkLength")) s.connectionStalkLength = toDouble(xml.readElementText(), s.connectionStalkLength); else if (nameIs(n, "preferredDistanceBetweenRooms")) { sawPreferredDistance = true; s.preferredDistanceBetweenRooms = toDouble(xml.readElementText(), s.preferredDistanceBetweenRooms); } else if (nameIs(n, "defaultRoomName")) { const QString name = xml.readElementText(); s.defaultRoomName = name.isEmpty() ? QStringLiteral("Cave") : name; } else if (nameIs(n, "defaultRoomShape")) s.defaultRoomShape = xml.readElementText().toInt(); else xml.skipCurrentElement(); } if (!sawPreferredDistance) s.preferredDistanceBetweenRooms = s.connectionStalkLength * 2.0; } void readUi(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "handleSize")) s.handleSize = toDouble(xml.readElementText(), s.handleSize); else if (nameIs(n, "snapToElementSize")) s.snapToElementSize = toDouble(xml.readElementText(), s.snapToElementSize); else xml.skipCurrentElement(); } } void readMargins(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "documentSpecific")) s.documentSpecificMargins = toBool(xml.readElementText(), s.documentSpecificMargins); else if (nameIs(n, "horizontal")) s.horizontalMargin = toDouble(xml.readElementText(), s.horizontalMargin); else if (nameIs(n, "vertical")) s.verticalMargin = toDouble(xml.readElementText(), s.verticalMargin); else if (nameIs(n, "wrapDashes")) s.wrapTextAtDashes = toBool(xml.readElementText(), s.wrapTextAtDashes); else xml.skipCurrentElement(); } } void readKeypad(QXmlStreamReader &xml, MapSettings &s) { while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "creationModifier")) s.keypadCreationModifier = normalizeModifierKeys(xml.readElementText(), s.keypadCreationModifier); else if (nameIs(n, "unexploredModifier")) s.keypadUnexploredModifier = normalizeModifierKeys(xml.readElementText(), s.keypadUnexploredModifier); else xml.skipCurrentElement(); } } void readSettings(QXmlStreamReader &xml, Map &out) { out.settings.loaded = true; while (xml.readNextStartElement()) { const auto n = xml.name(); if (nameIs(n, "colors")) readColors(xml, out.settings); else if (nameIs(n, "regions")) readRegions(xml, out); else if (nameIs(n, "fonts")) readFonts(xml, out.settings); else if (nameIs(n, "grid")) readGrid(xml, out.settings); else if (nameIs(n, "lines")) readLines(xml, out.settings); else if (nameIs(n, "rooms")) readRooms(xml, out.settings); else if (nameIs(n, "ui")) readUi(xml, out.settings); else if (nameIs(n, "margins")) readMargins(xml, out.settings); else if (nameIs(n, "keypadNavigation")) readKeypad(xml, out.settings); else xml.skipCurrentElement(); } } // Parse a document from an already-opened stream reader into out. `source` is a // human-readable label used in error messages. bool parseDocument(QXmlStreamReader &xml, Map &out, const QString &source, QString *errorMessage) { out.clear(); // A document has exactly one root element: read it once, then descend. if (xml.readNextStartElement()) { if (nameIs(xml.name(), "trizbort")) { out.version = attr(xml.attributes(), "version"); while (xml.readNextStartElement()) { const auto c = xml.name(); if (nameIs(c, "info")) readInfo(xml, out); else if (nameIs(c, "map")) readMap(xml, out); else if (nameIs(c, "settings")) readSettings(xml, out); else xml.skipCurrentElement(); } } else { xml.raiseError( QObject::tr("Not a Trizbort file (root element is <%1>)").arg(xml.name().toString())); } } if (xml.hasError()) { if (errorMessage) *errorMessage = QObject::tr("Parse error in %1 (line %2): %3") .arg(source) .arg(xml.lineNumber()) .arg(xml.errorString()); out.clear(); return false; } // If the file carried no , keep the default NoRegion, matching // Settings.Load's behavior of seeding a single default region. if (out.regions.isEmpty()) out.regions.append( Region{kNoRegion, QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}); // A legacy globally-hand-drawn map (yes>) carries no // per-room handDrawn attributes; apply the map-wide flag to every room so the // map keeps its hand-drawn look. It then re-saves in the modern per-room // form. Files that already state handDrawn per room are left untouched. if (out.settings.handDrawnGlobal && !out.settings.anyRoomHandDrawnExplicit) for (Room &r : out.rooms) r.handDrawn = true; out.reindex(); return true; } } // namespace bool TrizbortReader::load(const QString &path, Map &out, QString *errorMessage) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (errorMessage) *errorMessage = QObject::tr("Cannot open %1: %2").arg(path, file.errorString()); return false; } QXmlStreamReader xml(&file); return parseDocument(xml, out, path, errorMessage); } bool TrizbortReader::loadFromString(const QString &xmlText, Map &out, QString *errorMessage) { QXmlStreamReader xml(xmlText); return parseDocument(xml, out, QObject::tr("clipboard"), errorMessage); } } // namespace trizbort trizbort-qt-1.0.1/src/TrizbortReader.h000066400000000000000000000060051526521612000177150ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_TRIZBORTREADER_H #define TRIZBORT_TRIZBORTREADER_H #include #include "MapDocument.h" namespace trizbort { // Loads the .trizbort XML format into a Map. Returns false and sets // *errorMessage on failure. class TrizbortReader { public: static bool load(const QString &path, Map &out, QString *errorMessage = nullptr); // Parse a .trizbort document held in a string (used by clipboard paste). static bool loadFromString(const QString &xml, Map &out, QString *errorMessage = nullptr); }; // Parses a Trizbort color string ("#RRGGBB", a named color, or ""). An empty // or unrecognized string yields an invalid QColor, meaning "inherit default". QColor parseTrizbortColor(const QString &text); } // namespace trizbort #endif // TRIZBORT_TRIZBORTREADER_H trizbort-qt-1.0.1/src/TrizbortWriter.cpp000066400000000000000000000546301526521612000203310ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "TrizbortWriter.h" #include #include #include #include #include #include #include "DotNetColor.h" #include "TrizbortReader.h" namespace trizbort { namespace { // Settings/region colors are serialized by ColorTranslator.ToHtml, which is // provenance-sensitive in .NET (a color loaded as "#F5F5F5" stays hex; one // created from a KnownColor stays named). We can't recover provenance from the // color alone, so we keep the exact token each color was read from and reuse // it as long as the color is unchanged; an edited color falls back to ToHtml. QString colorWithToken(const QColor &c, const QString &token) { if (!token.isEmpty() && parseTrizbortColor(token) == c) return token; return dotNetColorToHtml(c); } // XmlScribe writes booleans as "yes"/"no". QString yesNo(bool b) { return b ? QStringLiteral("yes") : QStringLiteral("no"); } // Float attribute/element formatting: XmlScribe uses float.ToString with // InvariantCulture, i.e. the shortest round-tripping decimal. Values are stored // as double but originate as single-precision floats, so cast before printing. QString fmtFloat(double value) { char buf[64]; const std::to_chars_result res = std::to_chars(buf, buf + sizeof(buf), static_cast(value)); return QString::fromLatin1(buf, static_cast(res.ptr - buf)); } // Colors.SaveColor: empty for the default (invalid/transparent), else the // uppercase #RRGGBB hex (Color.ToHex). Used for room and connection colors. QString saveColorHex(const QColor &c) { if (!c.isValid()) return QString(); const unsigned int rgb = ((c.red() & 0xFF) << 16) | ((c.green() & 0xFF) << 8) | (c.blue() & 0xFF); return QStringLiteral("#%1").arg(rgb, 6, 16, QLatin1Char('0')).toUpper(); } QString escAttr(const QString &s) { QString r; r.reserve(s.size() + 8); for (const QChar c : s) { switch (c.unicode()) { case '&': r += QLatin1String("&"); break; case '<': r += QLatin1String("<"); break; case '>': r += QLatin1String(">"); break; case '"': r += QLatin1String("""); break; // Attribute values normalize whitespace, so newlines/tabs must be escaped // to survive a round trip (matching what .NET's XmlWriter emits). case '\n': r += QLatin1String(" "); break; case '\r': r += QLatin1String(" "); break; case '\t': r += QLatin1String(" "); break; default: r += c; break; } } return r; } QString escText(const QString &s) { QString r; r.reserve(s.size() + 8); for (const QChar c : s) { switch (c.unicode()) { case '&': r += QLatin1String("&"); break; case '<': r += QLatin1String("<"); break; case '>': r += QLatin1String(">"); break; case '\r': r += QLatin1String(" "); break; default: r += c; break; } } return r; } QString indent(int depth) { return QString(depth, QLatin1Char('\t')); } // One attribute, only when the value is non-empty-worthy per the caller. void addAttr(QString &out, const QString &name, const QString &value) { out += QLatin1Char(' '); out += name; out += QLatin1String("=\""); out += escAttr(value); out += QLatin1Char('"'); } // XmlConvert.EncodeName, used (after " "->"____") for a region's element tag. // Non-XML-name characters become _xHHHH_; a leading non-name-start char too. QString encodeXmlName(const QString &in) { QString s = in; s.replace(QLatin1Char(' '), QLatin1String("____")); auto isNameChar = [](QChar c, bool first) { const ushort u = c.unicode(); const bool letter = c.isLetter(); const bool digit = c.isDigit(); if (first) return letter || u == '_'; return letter || digit || c.isMark() || u == '_' || u == '-' || u == '.' || u == 0x00B7; }; QString r; for (int i = 0; i < s.size(); ++i) { const QChar c = s.at(i); if (isNameChar(c, i == 0)) { r += c; } else { r += QStringLiteral("_x%1_") .arg(static_cast(c.unicode()), 4, 16, QLatin1Char('0')) .toUpper(); } } if (r.isEmpty()) r = QStringLiteral("_"); return r; } void writeObjects(QString &out, const Room &r, int depth) { const QString &at = r.objectsPosition.at; const bool defaultPosition = at.isEmpty() || at == QLatin1String("s"); // South is default if (r.objectsText.isEmpty() && defaultPosition) return; QString openTag = indent(depth) + QLatin1String("\n"); return; } // Encode newlines as '|' (escaping literal pipes), matching Room.Save. QString encoded = r.objectsText; encoded.replace(QLatin1String("\r"), QString()); encoded.replace(QLatin1String("|"), QLatin1String("\\|")); encoded.replace(QLatin1String("\n"), QLatin1String("|")); out += openTag + QLatin1Char('>') + escText(encoded) + QLatin1String("\n"); } void writeRoom(QString &out, const Room &r, int depth) { QString tag = indent(depth) + QLatin1String("= 0) addAttr(tag, QStringLiteral("referenceRoom"), QString::number(r.referenceRoom)); addAttr(tag, QStringLiteral("handDrawn"), yesNo(r.handDrawn)); addAttr(tag, QStringLiteral("allcornersequal"), yesNo(r.allCornersEqual)); addAttr(tag, QStringLiteral("ellipse"), yesNo(r.ellipse)); addAttr(tag, QStringLiteral("roundedCorners"), yesNo(r.roundedCorners)); addAttr(tag, QStringLiteral("octagonal"), yesNo(r.octagonal)); addAttr(tag, QStringLiteral("cornerTopLeft"), fmtFloat(r.cornerTopLeft)); addAttr(tag, QStringLiteral("cornerTopRight"), fmtFloat(r.cornerTopRight)); addAttr(tag, QStringLiteral("cornerBottomLeft"), fmtFloat(r.cornerBottomLeft)); addAttr(tag, QStringLiteral("cornerBottomRight"), fmtFloat(r.cornerBottomRight)); addAttr(tag, QStringLiteral("borderstyle"), r.borderStyle); if (r.isDark) addAttr(tag, QStringLiteral("isDark"), yesNo(true)); if (r.isStartRoom) addAttr(tag, QStringLiteral("isStartRoom"), yesNo(true)); if (r.isEndRoom) addAttr(tag, QStringLiteral("isEndRoom"), yesNo(true)); addAttr(tag, QStringLiteral("description"), r.description); addAttr(tag, QStringLiteral("roomFill"), saveColorHex(r.fill)); addAttr(tag, QStringLiteral("secondFill"), saveColorHex(r.secondFill)); addAttr(tag, QStringLiteral("secondFillLocation"), r.secondFillLocation); addAttr(tag, QStringLiteral("roomBorder"), saveColorHex(r.border)); addAttr(tag, QStringLiteral("roomLargeText"), saveColorHex(r.largeText)); addAttr(tag, QStringLiteral("roomSubtitleColor"), saveColorHex(r.subtitleColor)); addAttr(tag, QStringLiteral("roomSmallText"), saveColorHex(r.smallText)); addAttr(tag, QStringLiteral("ZOrder"), QString::number(r.zOrder)); // Determine whether an child will be written. const QString &at = r.objectsPosition.at; const bool defaultPosition = at.isEmpty() || at == QLatin1String("s"); const bool hasObjects = !(r.objectsText.isEmpty() && defaultPosition); if (!hasObjects) { out += tag + QLatin1String(" />\n"); return; } out += tag + QLatin1String(">\n"); writeObjects(out, r, depth + 1); out += indent(depth) + QLatin1String("\n"); } void writeLine(QString &out, const Connection &c, int depth) { QString tag = indent(depth) + QLatin1String("\n"); return; } out += tag + QLatin1String(">\n"); int index = 0; for (const Vertex &v : c.vertices) { QString vtag = indent(depth + 1); if (v.docked) { vtag += QLatin1String("\n"); ++index; } out += indent(depth) + QLatin1String("\n"); } void writeElementText(QString &out, int depth, const QString &tag, const QString &text) { out += indent(depth) + QLatin1Char('<') + tag + QLatin1Char('>') + escText(text) + QLatin1String("\n"); } void writeFont(QString &out, int depth, const QString &tag, const FontSpec &f) { QString open = indent(depth) + QLatin1Char('<') + tag; addAttr(open, QStringLiteral("size"), fmtFloat(f.size)); if (f.bold) addAttr(open, QStringLiteral("bold"), yesNo(true)); if (f.italic) addAttr(open, QStringLiteral("italic"), yesNo(true)); if (f.underline) addAttr(open, QStringLiteral("underline"), yesNo(true)); if (f.strikeout) addAttr(open, QStringLiteral("strikeout"), yesNo(true)); out += open + QLatin1Char('>') + escText(f.family) + QLatin1String("\n"); } void writeSettings(QString &out, const Map &map, int depth) { const MapSettings &s = map.settings; out += indent(depth) + QLatin1String("\n"); // colors static const char *const kColorNames[ColorCount] = { "canvas", "border", "line", "selectedLine", "hoverLine", "subTitle", "smallText", "lineText", "grid", "startRoom", "endRoom"}; out += indent(depth + 1) + QLatin1String("\n"); for (int i = 0; i < ColorCount; ++i) writeElementText(out, depth + 2, QString::fromLatin1(kColorNames[i]), colorWithToken(s.colors[i], s.colorTokens[i])); out += indent(depth + 1) + QLatin1String("\n"); // regions, ordered by name (Regions.OrderBy(RegionName)) QList regions = map.regions; std::stable_sort(regions.begin(), regions.end(), [](const Region &a, const Region &b) { return a.name < b.name; }); out += indent(depth + 1) + QLatin1String("\n"); for (const Region &r : regions) { const QString tag = encodeXmlName(r.name); QString open = indent(depth + 2) + QLatin1Char('<') + tag; addAttr(open, QStringLiteral("Name"), r.name); addAttr(open, QStringLiteral("TextColor"), colorWithToken(r.textColor, r.textColorToken)); out += open + QLatin1Char('>') + escText(colorWithToken(r.fillColor, r.fillColorToken)) + QLatin1String("\n"); } out += indent(depth + 1) + QLatin1String("\n"); // fonts out += indent(depth + 1) + QLatin1String("\n"); writeFont(out, depth + 2, QStringLiteral("room"), s.roomFont); writeFont(out, depth + 2, QStringLiteral("object"), s.objectFont); writeFont(out, depth + 2, QStringLiteral("subTitle"), s.subtitleFont); writeFont(out, depth + 2, QStringLiteral("line"), s.lineFont); out += indent(depth + 1) + QLatin1String("\n"); // grid out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("snapTo"), yesNo(s.snapToGrid)); writeElementText(out, depth + 2, QStringLiteral("visible"), yesNo(s.gridVisible)); writeElementText(out, depth + 2, QStringLiteral("showOrigin"), yesNo(s.showOrigin)); writeElementText(out, depth + 2, QStringLiteral("size"), fmtFloat(s.gridSize)); out += indent(depth + 1) + QLatin1String("\n"); // lines out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("width"), fmtFloat(s.lineWidth)); writeElementText(out, depth + 2, QStringLiteral("arrowSize"), fmtFloat(s.arrowSize)); writeElementText(out, depth + 2, QStringLiteral("textOffset"), fmtFloat(s.textOffset)); out += indent(depth + 1) + QLatin1String("\n"); // rooms out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("darknessStripeSize"), fmtFloat(s.darknessStripeSize)); writeElementText(out, depth + 2, QStringLiteral("objectListOffset"), fmtFloat(s.objectListOffset)); writeElementText(out, depth + 2, QStringLiteral("connectionStalkLength"), fmtFloat(s.connectionStalkLength)); writeElementText(out, depth + 2, QStringLiteral("preferredDistanceBetweenRooms"), fmtFloat(s.preferredDistanceBetweenRooms)); writeElementText(out, depth + 2, QStringLiteral("defaultRoomName"), s.defaultRoomName); writeElementText(out, depth + 2, QStringLiteral("defaultRoomShape"), QString::number(s.defaultRoomShape)); out += indent(depth + 1) + QLatin1String("\n"); // ui out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("handleSize"), fmtFloat(s.handleSize)); writeElementText(out, depth + 2, QStringLiteral("snapToElementSize"), fmtFloat(s.snapToElementSize)); out += indent(depth + 1) + QLatin1String("\n"); // margins out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("documentSpecific"), yesNo(s.documentSpecificMargins)); writeElementText(out, depth + 2, QStringLiteral("horizontal"), fmtFloat(s.horizontalMargin)); writeElementText(out, depth + 2, QStringLiteral("vertical"), fmtFloat(s.verticalMargin)); writeElementText(out, depth + 2, QStringLiteral("wrapDashes"), yesNo(s.wrapTextAtDashes)); out += indent(depth + 1) + QLatin1String("\n"); // keypadNavigation out += indent(depth + 1) + QLatin1String("\n"); writeElementText(out, depth + 2, QStringLiteral("creationModifier"), s.keypadCreationModifier); writeElementText(out, depth + 2, QStringLiteral("unexploredModifier"), s.keypadUnexploredModifier); out += indent(depth + 1) + QLatin1String("\n"); out += indent(depth) + QLatin1String("\n"); } } // namespace QString TrizbortWriter::toString(const Map &map) { QString out; out += QLatin1String("\n"); out += QLatin1String("\n"); // const bool anyInfo = !map.title.isEmpty() || !map.author.isEmpty() || !map.description.isEmpty() || !map.history.isEmpty(); if (!anyInfo) { out += indent(1) + QLatin1String("\n"); } else { out += indent(1) + QLatin1String("\n"); if (!map.title.isEmpty()) writeElementText(out, 2, QStringLiteral("title"), map.title); if (!map.author.isEmpty()) writeElementText(out, 2, QStringLiteral("author"), map.author); if (!map.description.isEmpty()) writeElementText(out, 2, QStringLiteral("description"), map.description); if (!map.history.isEmpty()) writeElementText(out, 2, QStringLiteral("history"), map.history); out += indent(1) + QLatin1String("\n"); } // : rooms and lines are emitted in their original interleaved order // (by seq), so a load/save round trip preserves the element ordering. if (map.rooms.isEmpty() && map.connections.isEmpty()) { out += indent(1) + QLatin1String("\n"); } else { out += indent(1) + QLatin1String("\n"); int ri = 0; int ci = 0; while (ri < map.rooms.size() || ci < map.connections.size()) { const bool takeRoom = ci >= map.connections.size() || (ri < map.rooms.size() && map.rooms.at(ri).seq <= map.connections.at(ci).seq); if (takeRoom) writeRoom(out, map.rooms.at(ri++), 2); else writeLine(out, map.connections.at(ci++), 2); } out += indent(1) + QLatin1String("\n"); } // writeSettings(out, map, 1); // No trailing newline after the root element, matching Trizbort's output. out += QLatin1String(""); return out; } QString exportOutputPath(const QString &path) { if (!path.endsWith(QLatin1String(".inform"), Qt::CaseInsensitive)) return path; return path + QLatin1String("/Source/Story.ni"); } bool writeExportText(const QString &path, const QString &text, QString *errorMessage) { const QString target = exportOutputPath(path); if (target != path && !QDir().mkpath(QFileInfo(target).absolutePath())) { if (errorMessage) *errorMessage = QObject::tr("Cannot create %1").arg(QFileInfo(target).absolutePath()); return false; } QFile file(target); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if (errorMessage) *errorMessage = QObject::tr("Cannot write %1: %2").arg(target, file.errorString()); return false; } const QByteArray bytes = text.toUtf8(); if (file.write(bytes) != bytes.size()) { if (errorMessage) *errorMessage = QObject::tr("Error writing %1: %2").arg(target, file.errorString()); return false; } return true; } bool TrizbortWriter::save(const QString &path, const Map &map, QString *errorMessage) { QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if (errorMessage) *errorMessage = QObject::tr("Cannot write %1: %2").arg(path, file.errorString()); return false; } // A UTF-8 BOM, matching Trizbort's files (and the on-disk samples). Line // endings stay LF, as the repository stores them. QByteArray bytes = QByteArray::fromRawData("\xEF\xBB\xBF", 3) + toString(map).toUtf8(); if (file.write(bytes) != bytes.size()) { if (errorMessage) *errorMessage = QObject::tr("Error writing %1: %2").arg(path, file.errorString()); return false; } return true; } } // namespace trizbort trizbort-qt-1.0.1/src/TrizbortWriter.h000066400000000000000000000061441526521612000177730ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_TRIZBORTWRITER_H #define TRIZBORT_TRIZBORTWRITER_H #include #include "MapDocument.h" // Serializes a Map back to the .trizbort XML format, mirroring the C# // LegacyMapFileEngine.Save / Room.Save / Connection.Save / Settings.Save so a // round trip loses nothing and the output loads in stock Trizbort. namespace trizbort { class TrizbortWriter { public: // Serialize to a string (used by tests and by save()). static QString toString(const Map &map); // Serialize and write to a file. Returns false and sets errorMessage on I/O // failure. static bool save(const QString &path, const Map &map, QString *errorMessage = nullptr); }; QString exportOutputPath(const QString &path); bool writeExportText(const QString &path, const QString &text, QString *errorMessage = nullptr); } // namespace trizbort #endif // TRIZBORT_TRIZBORTWRITER_H trizbort-qt-1.0.1/src/Version.h.in000066400000000000000000000010721526521612000170040ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * GNU GPLv3 or later; see COPYING. * * Generated by CMake from Version.h.in -- do not edit. The version string is * resolved at configure time in CMakeLists.txt: the version declared in * project(), or a -DTRIZBORT_VERSION= override (used by the Debian build). */ #ifndef TRIZBORT_VERSION_H #define TRIZBORT_VERSION_H #define TRIZBORT_VERSION "@TRIZBORT_VERSION_STRING@" #endif // TRIZBORT_VERSION_H trizbort-qt-1.0.1/src/export/000077500000000000000000000000001526521612000161225ustar00rootroot00000000000000trizbort-qt-1.0.1/src/export/AlanExporter.cpp000066400000000000000000000274271526521612000212460ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "AlanExporter.h" namespace trizbort { namespace { bool isNullOrWhitespace(const QString &s) { return s.trimmed().isEmpty(); } bool containsOddCharacters(const QString &text) { for (const QChar c : text) if (c != QLatin1Char(' ') && c != QLatin1Char('-') && !c.isLetterOrNumber()) return true; return false; } QString alanString(const QString &text) { QString r = text; r.replace(QLatin1Char('"'), QLatin1String("\"\"")); return r; } QString alanQuotedId(const QString &text) { QString r = text; r.replace(QLatin1Char('\''), QLatin1String("''")); return r; } QString stripOddCharacters(const QString &text) { QString out; for (const QChar c : text) if (c == QLatin1Char(' ') || c == QLatin1Char('-') || c.isLetterOrNumber()) out += c; return out.isEmpty() ? QStringLiteral("object") : out; } bool containsWord(const QString &text, const QStringList &words) { const QStringList found = text.split(QLatin1Char(' '), Qt::SkipEmptyParts); for (const QString &word : words) for (const QString &f : found) if (QString::compare(word, f, Qt::CaseInsensitive) == 0) return true; return false; } QString getAlanName(MappableDirection direction) { switch (direction) { case MappableDirection::North: return QStringLiteral("North"); case MappableDirection::South: return QStringLiteral("South"); case MappableDirection::East: return QStringLiteral("East"); case MappableDirection::West: return QStringLiteral("West"); case MappableDirection::NorthEast: return QStringLiteral("Northeast"); case MappableDirection::SouthEast: return QStringLiteral("Southeast"); case MappableDirection::NorthWest: return QStringLiteral("Northwest"); case MappableDirection::SouthWest: return QStringLiteral("Southwest"); case MappableDirection::Up: return QStringLiteral("Up"); case MappableDirection::Down: return QStringLiteral("Down"); case MappableDirection::In: return QStringLiteral("Inside"); case MappableDirection::Out: return QStringLiteral("Outside"); default: return QString(); } } } // namespace QStringList AlanExporter::reservedWords() const { return {QStringLiteral("object"), QStringLiteral("objects"), QStringLiteral("thing"), QStringLiteral("things"), QStringLiteral("door"), QStringLiteral("doors"), QStringLiteral("is"), QStringLiteral("are"), QStringLiteral("in"), QStringLiteral("on"), QStringLiteral("and"), QStringLiteral("outside"), QStringLiteral("inside")}; } QString AlanExporter::alanName(const QString &nameIn, std::optional suffix) { // An Alan identifier is a single word: keep only letters and digits (drop // spaces, dashes and punctuation), and never begin with a digit. The // human-readable name is carried separately in a Name clause. QString name; for (const QChar c : nameIn) { const ushort u = c.unicode(); if ((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) name += c; } if (name.isEmpty()) name = QStringLiteral("object"); if (name.at(0).isDigit()) name = QLatin1Char('x') + name; if (containsWord(name, reservedWords()) && !suffix.has_value()) suffix = 1; if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } // The parser words for a Name clause: the display name split into words, each // stripped to letters/digits and quoted (so a word that is an Alan reserved // word or article is taken literally). Empty words are dropped. static QString alanNameWords(const QString &displayName) { QString result; const QStringList words = displayName.split(QLatin1Char(' '), Qt::SkipEmptyParts); for (const QString &w : words) { QString atom; for (const QChar c : w) { const ushort u = c.unicode(); if ((u >= 'A' && u <= 'Z') || (u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) atom += c; } if (atom.isEmpty()) continue; if (!result.isEmpty()) result += QLatin1Char(' '); result += QLatin1Char('\'') + atom + QLatin1Char('\''); } return result; } QString AlanExporter::getExportName(const Room &room, std::optional suffix) { return alanName(room.name, suffix); } QString AlanExporter::getExportName(const QString &displayName, std::optional suffix) { return alanName(displayName, suffix); } void AlanExporter::exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; wl(QStringLiteral("-- Alan currently does not process metadata such as ") + (isNullOrWhitespace(description) ? QStringLiteral("title and author") : QStringLiteral("title, author, or description")) + QStringLiteral(", so those will be in the metadata below.")); wl(QStringLiteral("-- Trizbort exports History to the 'about' verb.")); wl(QStringLiteral("-- All other metadata will be in comments below.")); wl(); wl(QStringLiteral("-- \"") + title + QStringLiteral("\" by ") + author); if (!isNullOrWhitespace(description)) { wl(QStringLiteral("-- description: ") + description + (description.endsWith(QLatin1Char('.')) ? QString() : QStringLiteral("."))); wl(); } if (!isNullOrWhitespace(history)) { QString historyCoded = alanString(history); historyCoded.replace(QLatin1String("\r\n"), QLatin1String("\n")); historyCoded.replace(QLatin1String("\n"), QLatin1String("\"\n \"")); wl(QStringLiteral("Verb about")); wl(QStringLiteral(" \"") + historyCoded + QLatin1Char('"')); wl(QStringLiteral("End Verb about.")); wl(QStringLiteral("")); } } void AlanExporter::printThisLoc(QString &out, Location *location) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; wl(QStringLiteral("The ") + location->exportName + QStringLiteral(" isa location Name '") + alanQuotedId(location->room->name) + QLatin1Char('\'')); const QString description = location->room->description; if (!isNullOrWhitespace(description)) { wl(QStringLiteral(" Description")); wl(QStringLiteral(" \"") + alanString(description) + QLatin1Char('"')); } else { wl(QStringLiteral(" Description \"\"")); } // One statement per exit. A simple exit is a single statement ended by a // period; only an exit with a Check block takes an End exit (and then no // period after the target). Directions that lead nowhere are left out // entirely -- Alan supplies the default "you can't go that way" refusal, so // there is no need to route them to a placeholder location. for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit == nullptr || exit->exported) continue; exit->exported = true; if (exit->conditional) { wl(QStringLiteral(" Exit ") + getAlanName(direction) + QStringLiteral(" to ") + exit->target->exportName); wl(QStringLiteral(" Check")); wl(QStringLiteral(" \"This was marked as a conditional exit in Trizbort, so " "you'll want to change it.\"")); wl(QStringLiteral(" End exit.")); } else { wl(QStringLiteral(" Exit ") + getAlanName(direction) + QStringLiteral(" to ") + exit->target->exportName + QLatin1Char('.')); } } if (location->room->isDark) wl(QStringLiteral(" Is Not lit.")); wl(QStringLiteral("end The ") + location->exportName + QLatin1Char('.')); wl(QStringLiteral("")); if (location->room->isStartRoom) { wl(QStringLiteral("The hero Isa actor at ") + location->exportName); wl(QStringLiteral("End The Hero.")); wl(); } for (const Thing *thing : location->things) { wl(QStringLiteral("The ") + thing->exportName + QStringLiteral(" isa ") + (thing->isPerson ? QStringLiteral("actor") : QStringLiteral("thing")) + QStringLiteral(" at ") + location->exportName + QLatin1Char('.')); // Give the object its parser words. (IsDisplayedAs is not Alan syntax.) const QString nameWords = alanNameWords(thing->displayName); if (!nameWords.isEmpty()) wl(QStringLiteral(" Name ") + nameWords + QLatin1Char('.')); wl(QStringLiteral("End The ") + thing->exportName + QLatin1Char('.')); wl(); } } void AlanExporter::exportContent(QString &out) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; for (Location *location : m_locationsInExportOrder) printThisLoc(out, location); // Alan requires an explicit start location (an instance inheriting from // 'location'); use the map's start room, falling back to the first room. if (!m_locationsInExportOrder.isEmpty()) { Location *start = m_locationsInExportOrder.first(); for (Location *location : m_locationsInExportOrder) if (location->room->isStartRoom) { start = location; break; } wl(QStringLiteral("Start At ") + start->exportName + QLatin1Char('.')); } } } // namespace trizbort trizbort-qt-1.0.1/src/export/AlanExporter.h000066400000000000000000000061761526521612000207110ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_ALANEXPORTER_H #define TRIZBORT_EXPORT_ALANEXPORTER_H #include "CodeExporter.h" // ALAN 3 exporter, ported from AlanExporter.cs. namespace trizbort { class AlanExporter : public CodeExporter { public: using CodeExporter::CodeExporter; protected: QStringList reservedWords() const override; void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) override; void exportContent(QString &out) override; QString getExportName(const Room &room, std::optional suffix) override; QString getExportName(const QString &displayName, std::optional suffix) override; private: QString alanName(const QString &name, std::optional suffix); void printThisLoc(QString &out, Location *location); }; } // namespace trizbort #endif // TRIZBORT_EXPORT_ALANEXPORTER_H trizbort-qt-1.0.1/src/export/CodeExporter.cpp000066400000000000000000000250411526521612000212330ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "CodeExporter.h" #include #include #include namespace trizbort { CodeExporter::CodeExporter(const Map &map, const QString &sourcePath) : m_map(map), m_sourcePath(sourcePath) { } CodeExporter::~CodeExporter() { qDeleteAll(m_ownedExits); qDeleteAll(m_ownedThings); qDeleteAll(m_ownedLocations); qDeleteAll(m_ownedRegions); } QString CodeExporter::deaccent(const QString &s) { QString x; x.reserve(s.size()); for (const QChar qc : s) { const ushort c = qc.unicode(); if (c >= 0x00E0 && c <= 0x00E5) x += QLatin1Char('a'); else if (c >= 0x00C0 && c <= 0x00C5) x += QLatin1Char('A'); else if (c == 0x00C7) x += QLatin1Char('C'); else if (c == 0x00E7) x += QLatin1Char('c'); else if (c >= 0x00E8 && c <= 0x00EB) x += QLatin1Char('e'); else if (c >= 0x00C8 && c <= 0x00CB) x += QLatin1Char('E'); else if (c >= 0x00EC && c <= 0x00EF) x += QLatin1Char('i'); else if (c >= 0x00CC && c <= 0x00CF) x += QLatin1Char('I'); else if (c == 0x00F1) x += QLatin1Char('n'); else if (c == 0x00D1) x += QLatin1Char('N'); else if (c >= 0x00D2 && c <= 0x00D6) x += QLatin1Char('o'); // C# quirk: upper -> lower else if (c >= 0x00F2 && c <= 0x00F6) x += QLatin1Char('O'); // C# quirk: lower -> upper else if (c >= 0x00F9 && c <= 0x00FC) x += QLatin1Char('u'); else if (c >= 0x00D9 && c <= 0x00DC) x += QLatin1Char('U'); else x += qc; } return x; } QString CodeExporter::exportToString() { QString title = m_map.title; if (title.isEmpty()) { title = QFileInfo(m_sourcePath).completeBaseName(); if (title.isEmpty()) title = QStringLiteral("A Trizbort Map"); } QString author = m_map.author; if (author.isEmpty()) author = QStringLiteral("A Trizbort User"); const QString history = m_map.history; prepareContent(); QString out; exportHeader(out, title, author, m_map.description, history); exportContent(out); return out; } void CodeExporter::prepareContent() { findRegions(); findRooms(); findExits(); pickBestExits(); findThings(); } void CodeExporter::findRegions() { QSet used; for (const QString &word : reservedWords()) used.insert(word.toLower()); for (const Region ®ion : m_map.regions) { if (isRegionless(region.name)) continue; QString exportName = getExportName(region.name, std::nullopt); if (exportName.isEmpty()) exportName = QStringLiteral("region"); int index = 2; while (used.contains(exportName.toLower())) exportName = getExportName(region.name, index++); used.insert(exportName.toLower()); auto *er = new ExportRegion{region, exportName}; m_ownedRegions.append(er); m_regionsInExportOrder.append(er); } } void CodeExporter::findRooms() { QSet used; for (const QString &word : reservedWords()) used.insert(word.toLower()); for (const ExportRegion *er : m_regionsInExportOrder) used.insert(er->exportName.toLower()); for (const Room &room : m_map.rooms) { QString exportName = getExportName(room, std::nullopt); if (exportName.isEmpty()) exportName = QStringLiteral("object"); int index = 2; while (used.contains(exportName.toLower())) exportName = getExportName(room, index++); used.insert(exportName.toLower()); auto *loc = new Location(&room, exportName); m_ownedLocations.append(loc); m_locationsInExportOrder.append(loc); m_roomIdToLocation.insert(room.id, loc); } } const Room *CodeExporter::sourceRoom(const Connection &c, CompassPoint &cp) const { QList v = c.vertices; std::sort(v.begin(), v.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); if (!v.isEmpty() && v.first().docked && compassPointFromPort(v.first().port, cp)) return m_map.roomById(v.first().roomId); cp = CompassPoint::North; return nullptr; } const Room *CodeExporter::targetRoom(const Connection &c, CompassPoint &cp) const { QList v = c.vertices; std::sort(v.begin(), v.end(), [](const Vertex &a, const Vertex &b) { return a.index < b.index; }); if (v.size() > 1 && v.last().docked && compassPointFromPort(v.last().port, cp)) return m_map.roomById(v.last().roomId); cp = CompassPoint::North; return nullptr; } void CodeExporter::findExits() { for (const Connection &connection : m_map.connections) { CompassPoint sourceCp = CompassPoint::North; CompassPoint targetCp = CompassPoint::North; const Room *src = sourceRoom(connection, sourceCp); const Room *tgt = targetRoom(connection, targetCp); if (src == nullptr || tgt == nullptr) continue; if (src == tgt && sourceCp == targetCp) continue; Location *srcLoc = m_roomIdToLocation.value(src->id, nullptr); Location *tgtLoc = m_roomIdToLocation.value(tgt->id, nullptr); if (srcLoc == nullptr || tgtLoc == nullptr) continue; auto *e1 = new Exit(srcLoc, tgtLoc, sourceCp, connection.startText, connection); m_ownedExits.append(e1); srcLoc->addExit(e1); if (connection.flow == ConnectionFlow::TwoWay) { auto *e2 = new Exit(tgtLoc, srcLoc, targetCp, connection.endText, connection); m_ownedExits.append(e2); tgtLoc->addExit(e2); } } } void CodeExporter::pickBestExits() { for (Location *loc : m_locationsInExportOrder) loc->pickBestExits(); } void CodeExporter::findThings() { QSet used; for (const QString &word : reservedWords()) used.insert(word.toLower()); for (const Location *loc : m_locationsInExportOrder) used.insert(loc->exportName.toLower()); for (const ExportRegion *er : m_regionsInExportOrder) used.insert(er->exportName.toLower()); const QRegularExpression bracket(QStringLiteral("\\[[^\\]\\[]*\\]")); const QRegularExpression throughLastOpen(QStringLiteral(".*\\[")); const QRegularExpression fromFirstClose(QStringLiteral("\\].*")); for (Location *loc : m_locationsInExportOrder) { const QString objectsText = loc->room->objectsText; if (objectsText.isEmpty()) continue; QString normalized = objectsText; normalized.remove(QLatin1Char('\r')); const QStringList objectNames = normalized.split(QLatin1Char('\n'), Qt::SkipEmptyParts); for (const QString &objectName : objectNames) { QString displayName = objectName.trimmed(); QString propString; const QRegularExpressionMatch match = bracket.match(displayName); if (match.hasMatch()) { propString = displayName; displayName.remove(bracket); propString.remove(throughLastOpen); propString.remove(fromFirstClose); } if (displayName.isEmpty()) continue; QString exportName = getExportName(displayName, std::nullopt); int index = 2; while (used.contains(exportName.toLower())) exportName = getExportName(displayName, index++); used.insert(exportName.toLower()); int indent = 0; while (indent < objectName.size() && objectName.at(indent) == QLatin1Char(' ')) ++indent; Thing *container = nullptr; for (qsizetype i = loc->things.size() - 1; i >= 0; --i) { Thing *prior = loc->things.at(i); if (indent > prior->indent) { container = prior; break; } } auto *thing = new Thing; thing->displayName = displayName; thing->exportName = exportName; thing->location = loc; thing->container = container; thing->indent = indent; thing->propString = propString; thing->parseProperties(); m_ownedThings.append(thing); if (container != nullptr) container->contents.append(thing); loc->things.append(thing); } } } } // namespace trizbort trizbort-qt-1.0.1/src/export/CodeExporter.h000066400000000000000000000102101526521612000206700ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_CODEEXPORTER_H #define TRIZBORT_EXPORT_CODEEXPORTER_H #include #include #include #include #include #include "../MapDocument.h" #include "ExportModel.h" // Base class for the code exporters, ported from Export/CodeExporter.cs. // Builds the intermediate model (regions, locations, exits, things) from a Map, // then a subclass serializes it to a target language. namespace trizbort { class CodeExporter { public: CodeExporter(const Map &map, const QString &sourcePath); virtual ~CodeExporter(); // Runs the pipeline and returns the full exported source. QString exportToString(); protected: virtual QStringList reservedWords() const = 0; virtual void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) = 0; virtual void exportContent(QString &out) = 0; virtual QString getExportName(const Room &room, std::optional suffix) = 0; virtual QString getExportName(const QString &displayName, std::optional suffix) = 0; static QString deaccent(const QString &s); const Map &m_map; QString m_sourcePath; QList m_locationsInExportOrder; QList m_regionsInExportOrder; private: void prepareContent(); void findRegions(); void findRooms(); void findExits(); void pickBestExits(); void findThings(); const Room *sourceRoom(const Connection &c, CompassPoint &cp) const; const Room *targetRoom(const Connection &c, CompassPoint &cp) const; QHash m_roomIdToLocation; // Owned heap objects, freed in the destructor. QList m_ownedLocations; QList m_ownedExits; QList m_ownedThings; QList m_ownedRegions; }; } // namespace trizbort #endif // TRIZBORT_EXPORT_CODEEXPORTER_H trizbort-qt-1.0.1/src/export/ExportModel.cpp000066400000000000000000000400501526521612000210670ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "ExportModel.h" #include #include #include "../AppSettings.h" namespace trizbort { const QList &allDirections() { static const QList dirs = { MappableDirection::North, MappableDirection::South, MappableDirection::East, MappableDirection::West, MappableDirection::NorthEast, MappableDirection::SouthEast, MappableDirection::SouthWest, MappableDirection::NorthWest, MappableDirection::Up, MappableDirection::Down, MappableDirection::In, MappableDirection::Out, MappableDirection::None}; return dirs; } bool compassPointFromPort(const QString &port, CompassPoint &out) { // Index order matches CompassPointHelper.Names and the CompassPoint enum. static const char *const names[16] = { "n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"}; const QString p = port.toLower(); for (int i = 0; i < 16; ++i) { if (p == QLatin1String(names[i])) { out = static_cast(i); return true; } } return false; } MappableDirection oppositeOf(MappableDirection direction) { switch (direction) { case MappableDirection::North: return MappableDirection::South; case MappableDirection::South: return MappableDirection::North; case MappableDirection::East: return MappableDirection::West; case MappableDirection::West: return MappableDirection::East; case MappableDirection::NorthEast: return MappableDirection::SouthWest; case MappableDirection::NorthWest: return MappableDirection::SouthEast; case MappableDirection::SouthEast: return MappableDirection::NorthWest; case MappableDirection::SouthWest: return MappableDirection::NorthEast; case MappableDirection::Up: return MappableDirection::Down; case MappableDirection::Down: return MappableDirection::Up; case MappableDirection::In: return MappableDirection::Out; case MappableDirection::Out: return MappableDirection::In; default: return MappableDirection::North; } } // --- Exit --- QString roomTooltipText(const Room &room) { const AppSettings &app = AppSettings::instance(); if (!app.showDescriptionsInTooltips) return QString(); QString desc = room.description; if (app.limitRoomDescriptionChars && app.roomDescriptionChars >= 0 && desc.length() >= app.roomDescriptionChars) desc = desc.left(app.roomDescriptionChars) + QStringLiteral("..."); return desc; } Exit::Exit(Location *from, Location *to, CompassPoint compassPoint, const QString &connectionText, const Connection &connection) : source(from), target(to), visualCompassPoint(compassPoint) { conditional = (connection.style == ConnectionStyle::Dashed); hasDoor = connection.hasDoor; door = connection.door; connectionName = connection.name; connectionDescription = connection.description; // Order matters and is preserved from the C#: priority is computed while // secondaryDirection is still unset, which makes the negative branches dead. assignPrimaryPriority(); assignSecondaryDirection(connectionText); if (secondaryDirection.has_value()) primaryDirection = secondaryDirection.value(); else assignPrimaryDirection(); } void Exit::assignPrimaryPriority() { m_primaryPriority = 0; switch (visualCompassPoint) { case CompassPoint::North: case CompassPoint::South: case CompassPoint::East: case CompassPoint::West: case CompassPoint::NorthEast: case CompassPoint::SouthEast: case CompassPoint::SouthWest: case CompassPoint::NorthWest: if (!secondaryDirection.has_value()) m_primaryPriority += 4; else m_primaryPriority -= 2; break; default: if (!secondaryDirection.has_value()) m_primaryPriority += 3; else m_primaryPriority -= 1; break; } } void Exit::assignSecondaryDirection(const QString &connectionText) { if (connectionText == QLatin1String("up")) secondaryDirection = MappableDirection::Up; else if (connectionText == QLatin1String("down")) secondaryDirection = MappableDirection::Down; else if (connectionText == QLatin1String("in")) secondaryDirection = MappableDirection::In; else if (connectionText == QLatin1String("out")) secondaryDirection = MappableDirection::Out; else secondaryDirection.reset(); } void Exit::assignPrimaryDirection() { switch (visualCompassPoint) { case CompassPoint::NorthNorthWest: case CompassPoint::North: case CompassPoint::NorthNorthEast: primaryDirection = MappableDirection::North; break; case CompassPoint::NorthEast: primaryDirection = MappableDirection::NorthEast; break; case CompassPoint::EastNorthEast: case CompassPoint::East: case CompassPoint::EastSouthEast: primaryDirection = MappableDirection::East; break; case CompassPoint::SouthEast: primaryDirection = MappableDirection::SouthEast; break; case CompassPoint::SouthSouthEast: case CompassPoint::South: case CompassPoint::SouthSouthWest: primaryDirection = MappableDirection::South; break; case CompassPoint::SouthWest: primaryDirection = MappableDirection::SouthWest; break; case CompassPoint::WestSouthWest: case CompassPoint::West: case CompassPoint::WestNorthWest: primaryDirection = MappableDirection::West; break; case CompassPoint::NorthWest: primaryDirection = MappableDirection::NorthWest; break; } } int Exit::priority(MappableDirection direction) const { if (direction == primaryDirection) return m_primaryPriority; if (secondaryDirection.has_value() && direction == secondaryDirection.value()) return 1; return -1; } bool Exit::isReciprocated(Location *source, MappableDirection direction, Location *target) { if (target != nullptr) { const MappableDirection opposite = oppositeOf(direction); Exit *reciprocal = target->bestExit(opposite); if (reciprocal != nullptr) return reciprocal->target == source; } return false; } // --- Thing --- void Thing::parseProperties() { auto genderName = [](Gender g) -> QString { switch (g) { case Gender::Neuter: return QStringLiteral("Neuter"); case Gender::Male: return QStringLiteral("Male"); case Gender::Female: return QStringLiteral("Female"); } return QString(); }; QString errString = propString; errString.remove(QRegularExpression(QStringLiteral("[fmp12csuwh!]"))); if (!errString.trimmed().isEmpty()) warningText += QStringLiteral("The properties string ") + propString + QStringLiteral(" has the invalid character") + (errString.length() == 1 ? QString() : QStringLiteral("s")) + QLatin1Char(' ') + errString + QStringLiteral(".\n"); if (propString.contains(QLatin1Char('f'))) { isPerson = true; gender = Gender::Female; } if (propString.contains(QLatin1Char('m'))) { if (isPerson) warningText += QStringLiteral("You defined two different genders: ") + genderName(gender) + QStringLiteral(" then male.\n"); gender = Gender::Male; isPerson = true; } if (propString.contains(QLatin1Char('p'))) { if (isPerson) warningText += QStringLiteral("You defined two different genders: ") + genderName(gender) + QStringLiteral(" then neuter.\n"); gender = Gender::Neuter; isPerson = true; } if (propString.contains(QLatin1Char('w'))) worn = true; if (propString.contains(QLatin1Char('h'))) partOf = true; forceplural = Amounts::Noforce; if (propString.contains(QLatin1Char('1'))) forceplural = Amounts::Singular; if (propString.contains(QLatin1Char('2'))) { if (forceplural != Amounts::Noforce) warningText += QStringLiteral("You defined this object as both singular and plural.\n"); forceplural = Amounts::Plural; } if (propString.contains(QLatin1Char('c'))) { if (isPerson) warningText += QStringLiteral("You defined this as a person and container. This will " "cause Inform to throw an error.\n"); else isContainer = true; } if (propString.contains(QLatin1Char('s'))) { if (isPerson) warningText += QStringLiteral("You defined this as a person and scenery. Inform allows " "that, but you may not want to hide this person.\n"); isScenery = true; } if (propString.contains(QLatin1Char('u'))) { if (isPerson) warningText += QStringLiteral("You defined this as a person and a supporter. This will " "cause Inform to throw an error.\n"); else isSupporter = true; } if (propString.contains(QLatin1Char('!'))) properNamed = true; } // --- Location --- namespace { // Reproduce .NET's List.Sort (introspective sort) exactly, because the // exporters depend on how it orders equal-priority exits. In particular it // uses fixed swap networks for 2- and 3-element partitions, which reorder ties // differently from a stable sort. Comparison mirrors the C#: b - a (descending // by priority for the given direction). int cmpExit(Exit *a, Exit *b, MappableDirection dir) { return b->priority(dir) - a->priority(dir); } void swapItems(QList &a, int i, int j) { Exit *t = a[i]; a[i] = a[j]; a[j] = t; } void swapIfGreater(QList &a, MappableDirection dir, int i, int j) { if (i != j && cmpExit(a[i], a[j], dir) > 0) swapItems(a, i, j); } void insertionSort(QList &a, int lo, int hi, MappableDirection dir) { for (int i = lo; i < hi; ++i) { Exit *t = a[i + 1]; int j = i; while (j >= lo && cmpExit(t, a[j], dir) < 0) { a[j + 1] = a[j]; --j; } a[j + 1] = t; } } void downHeap(QList &a, int i, int n, int lo, MappableDirection dir) { Exit *d = a[lo + i - 1]; while (i <= n / 2) { int child = 2 * i; if (child < n && cmpExit(a[lo + child - 1], a[lo + child], dir) < 0) ++child; if (!(cmpExit(d, a[lo + child - 1], dir) < 0)) break; a[lo + i - 1] = a[lo + child - 1]; i = child; } a[lo + i - 1] = d; } void heapSort(QList &a, int lo, int hi, MappableDirection dir) { const int n = hi - lo + 1; for (int i = n / 2; i >= 1; --i) downHeap(a, i, n, lo, dir); for (int i = n; i > 1; --i) { swapItems(a, lo, lo + i - 1); downHeap(a, 1, i - 1, lo, dir); } } int pickPivotAndPartition(QList &a, int lo, int hi, MappableDirection dir) { const int mid = lo + (hi - lo) / 2; swapIfGreater(a, dir, lo, mid); swapIfGreater(a, dir, lo, hi); swapIfGreater(a, dir, mid, hi); Exit *pivot = a[mid]; swapItems(a, mid, hi - 1); int left = lo, right = hi - 1; while (left < right) { while (cmpExit(a[++left], pivot, dir) < 0) { } while (cmpExit(pivot, a[--right], dir) < 0) { } if (left >= right) break; swapItems(a, left, right); } if (left != hi - 1) swapItems(a, left, hi - 1); return left; } void introSort(QList &a, int lo, int hi, int depthLimit, MappableDirection dir) { while (hi > lo) { const int partitionSize = hi - lo + 1; if (partitionSize <= 16) { if (partitionSize == 1) return; if (partitionSize == 2) { swapIfGreater(a, dir, lo, hi); return; } if (partitionSize == 3) { swapIfGreater(a, dir, lo, hi - 1); swapIfGreater(a, dir, lo, hi); swapIfGreater(a, dir, hi - 1, hi); return; } insertionSort(a, lo, hi, dir); return; } if (depthLimit == 0) { heapSort(a, lo, hi, dir); return; } --depthLimit; const int p = pickPivotAndPartition(a, lo, hi, dir); introSort(a, p + 1, hi, depthLimit, dir); hi = p - 1; } } int floorLog2(int n) { int r = 0; while ((n >>= 1) != 0) ++r; return r; } void dotnetListSort(QList &a, MappableDirection dir) { if (a.size() > 1) introSort(a, 0, static_cast(a.size()) - 1, 2 * (floorLog2(static_cast(a.size())) + 1), dir); } } // namespace Exit *Location::bestExit(MappableDirection direction) const { return m_bestByDirection.value(static_cast(direction), nullptr); } void Location::pickBestExits() { m_bestByDirection.clear(); for (MappableDirection direction : allDirections()) { Exit *exit = pickBestExit(direction); if (exit != nullptr) m_bestByDirection.insert(static_cast(direction), exit); } } Exit *Location::pickBestExit(MappableDirection direction) { // Sort in place exactly as the C# does (mExits.Sort with a b-a comparer), // reproducing .NET's introspective sort so equal-priority ties break the // same way byte-for-byte. dotnetListSort(m_exits, direction); if (!m_exits.isEmpty()) { Exit *exit = m_exits.first(); if (exit->primaryDirection == direction || (exit->secondaryDirection.has_value() && exit->secondaryDirection.value() == direction)) return exit; } return nullptr; } } // namespace trizbort trizbort-qt-1.0.1/src/export/ExportModel.h000066400000000000000000000135651526521612000205470ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_MODEL_H #define TRIZBORT_EXPORT_MODEL_H #include #include #include #include #include "../MapDocument.h" // The intermediate export model, ported from Export/Domain/{Exit,Location,Thing, // ExportRegion}.cs and the direction/compass helpers in Domain/Misc/CompassPoint.cs. namespace trizbort { enum class MappableDirection { North, South, East, West, NorthEast, SouthEast, SouthWest, NorthWest, Up, Down, In, Out, None }; // 16-point compass, same order as the C# CompassPoint enum. enum class CompassPoint { North, NorthNorthEast, NorthEast, EastNorthEast, East, EastSouthEast, SouthEast, SouthSouthEast, South, SouthSouthWest, SouthWest, WestSouthWest, West, WestNorthWest, NorthWest, NorthNorthWest }; // Directions in MappableDirection enum order (== C# Directions.AllDirections). const QList &allDirections(); // Dock port string ("n","ne","nnw",...) -> CompassPoint. false if unrecognized. bool compassPointFromPort(const QString &port, CompassPoint &out); inline bool isRegionless(const QString ®ion) { return region.isEmpty() || region == kNoRegion; } QString roomTooltipText(const Room &room); MappableDirection oppositeOf(MappableDirection direction); class Location; // One direction of a connection (a two-way connection yields two Exits). class Exit { public: Exit(Location *source, Location *target, CompassPoint visualCompassPoint, const QString &connectionText, const Connection &connection); Location *source = nullptr; Location *target = nullptr; CompassPoint visualCompassPoint = CompassPoint::North; bool conditional = false; // dashed connection bool exported = false; bool hasDoor = false; Door door; QString connectionName; QString connectionDescription; MappableDirection primaryDirection = MappableDirection::North; std::optional secondaryDirection; int priority(MappableDirection direction) const; static bool isReciprocated(Location *source, MappableDirection direction, Location *target); private: int m_primaryPriority = 0; void assignPrimaryPriority(); void assignSecondaryDirection(const QString &connectionText); void assignPrimaryDirection(); }; // One object parsed from a room's Objects text. class Thing { public: enum class Amounts { Noforce, Singular, Plural }; enum class Gender { Neuter, Male, Female }; QString displayName; QString exportName; Location *location = nullptr; Thing *container = nullptr; int indent = 0; QString propString; QList contents; // Derived from propString (see parseProperties), ported from Thing.cs. bool isPerson = false; Gender gender = Gender::Neuter; bool isContainer = false; bool isScenery = false; bool isSupporter = false; bool worn = false; bool partOf = false; bool properNamed = false; Amounts forceplural = Amounts::Noforce; QString warningText; void parseProperties(); }; class ExportRegion { public: Region region; QString exportName; }; class Location { public: Location(const Room *theRoom, const QString &name) : room(theRoom), exportName(name) {} const Room *room = nullptr; QString exportName; QList things; void addExit(Exit *exit) { m_exits.append(exit); } Exit *bestExit(MappableDirection direction) const; void pickBestExits(); private: Exit *pickBestExit(MappableDirection direction); QList m_exits; QHash m_bestByDirection; // int(MappableDirection) -> Exit }; } // namespace trizbort #endif // TRIZBORT_EXPORT_MODEL_H trizbort-qt-1.0.1/src/export/ExporterFactory.cpp000066400000000000000000000046351526521612000217760ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #include "ExporterFactory.h" #include "AlanExporter.h" #include "Inform6Exporter.h" #include "Inform7Exporter.h" #include "QuestExporter.h" #include "ZilExporter.h" namespace trizbort { const QList &exportFormats() { static const QList formats = { {QStringLiteral("zil"), QStringLiteral("ZIL (Infocom / ZILF)"), QStringLiteral("zil")}, {QStringLiteral("inform7"), QStringLiteral("Inform 7"), QStringLiteral("ni")}, {QStringLiteral("inform6"), QStringLiteral("Inform 6"), QStringLiteral("inf")}, {QStringLiteral("alan"), QStringLiteral("Alan"), QStringLiteral("i")}, {QStringLiteral("quest"), QStringLiteral("Quest 5"), QStringLiteral("aslx")}, {QStringLiteral("questrooms"), QStringLiteral("Quest 5 (rooms only)"), QStringLiteral("aslx")}, }; return formats; } std::unique_ptr makeExporter(const QString &fmt, const Map &map, const QString &path) { if (fmt == QLatin1String("zil")) return std::make_unique(map, path); if (fmt == QLatin1String("alan")) return std::make_unique(map, path); if (fmt == QLatin1String("inform6")) return std::make_unique(map, path); if (fmt == QLatin1String("inform7")) return std::make_unique(map, path); if (fmt == QLatin1String("quest")) return std::make_unique(map, path); if (fmt == QLatin1String("questrooms")) return std::make_unique(map, path); return nullptr; } } // namespace trizbort trizbort-qt-1.0.1/src/export/ExporterFactory.h000066400000000000000000000032711526521612000214360ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-License-Identifier: GPL-3.0-or-later * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . */ #ifndef TRIZBORT_EXPORT_EXPORTERFACTORY_H #define TRIZBORT_EXPORT_EXPORTERFACTORY_H #include #include #include #include "../MapDocument.h" #include "CodeExporter.h" namespace trizbort { // One selectable export target: the CLI/format key, a menu label, and the file // extension its output uses. struct ExportFormat { QString key; // "zil", "inform7", ... QString label; // "ZIL (Infocom)" QString extension; // "zil", "ni", ... }; // The eight code-generation targets, in menu order. const QList &exportFormats(); // Construct an exporter for the given format key, or nullptr if unknown. std::unique_ptr makeExporter(const QString &fmt, const Map &map, const QString &path); } // namespace trizbort #endif // TRIZBORT_EXPORT_EXPORTERFACTORY_H trizbort-qt-1.0.1/src/export/Inform6Exporter.cpp000066400000000000000000000440051526521612000217020ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "Inform6Exporter.h" #include namespace trizbort { namespace { const QChar SINGLE_QUOTE = QLatin1Char('\''); const QChar DOUBLE_QUOTE = QLatin1Char('"'); bool isAccentKept(ushort c) { return (c >= 0x00C0 && c <= 0x00CF) || c == 0x00D1 || (c >= 0x00D2 && c <= 0x00D6) || (c >= 0x00D9 && c <= 0x00DC) || c == 0x00DF || (c >= 0x00E0 && c <= 0x00EF) || c == 0x00F1 || (c >= 0x00F2 && c <= 0x00F6) || (c >= 0x00F9 && c <= 0x00FC); } // stripOddCharacters(text, accented..., ' ', '-'): keep alnum-ascii, '_', the // accented set, space and dash. QString stripUnaccentedCharacters(const QString &text) { QString result; for (const QChar qc : text) { const ushort c = qc.unicode(); if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || qc == QLatin1Char(' ') || qc == QLatin1Char('-') || isAccentKept(c)) result += qc; } return result; } // Characters are emitted verbatim (UTF-8); the "!% -Cu" header exportHeader // writes makes the compiler read this source as UTF-8, so accented letters and // other Unicode appear directly, with no @-escapes and no loss. What each // character can then be *stored* as depends on the compile target: the Z-machine // holds only its ZSCII set (ASCII plus the standard accented Latin letters and a // few symbols such as the pound sign); Glulx handles the full Unicode range. We // do not know the intended target, so we neither transliterate nor substitute // here -- the author compiles with -G for Glulx if the map uses characters // beyond ZSCII. QString toI6String(const QString &text, QChar quote) { QString r = text; r.replace(DOUBLE_QUOTE, QLatin1Char('~')); r.remove(QLatin1Char('\r')); r.replace(QLatin1Char('\n'), QLatin1Char('^')); return quote + r + quote; } QString toI6PropertyName(MappableDirection direction) { switch (direction) { case MappableDirection::North: return QStringLiteral("n_to"); case MappableDirection::South: return QStringLiteral("s_to"); case MappableDirection::East: return QStringLiteral("e_to"); case MappableDirection::West: return QStringLiteral("w_to"); case MappableDirection::NorthEast: return QStringLiteral("ne_to"); case MappableDirection::NorthWest: return QStringLiteral("nw_to"); case MappableDirection::SouthEast: return QStringLiteral("se_to"); case MappableDirection::SouthWest: return QStringLiteral("sw_to"); case MappableDirection::Up: return QStringLiteral("u_to"); case MappableDirection::Down: return QStringLiteral("d_to"); case MappableDirection::In: return QStringLiteral("in_to"); case MappableDirection::Out: return QStringLiteral("out_to"); default: return QStringLiteral("north"); } } } // namespace QStringList Inform6Exporter::reservedWords() const { return {QStringLiteral("Constant"), QStringLiteral("Story"), QStringLiteral("Headline"), QStringLiteral("Include"), QStringLiteral("Object"), QStringLiteral("with"), QStringLiteral("has"), QStringLiteral("hasnt"), QStringLiteral("not"), QStringLiteral("and"), QStringLiteral("or"), QStringLiteral("n_to"), QStringLiteral("s_to"), QStringLiteral("e_to"), QStringLiteral("w_to"), QStringLiteral("nw_to"), QStringLiteral("ne_to"), QStringLiteral("sw_to"), QStringLiteral("se_to"), QStringLiteral("u_to"), QStringLiteral("d_to"), QStringLiteral("in_to"), QStringLiteral("out_to"), QStringLiteral("before"), QStringLiteral("after"), QStringLiteral("if"), QStringLiteral("else"), QStringLiteral("print"), QStringLiteral("player"), QStringLiteral("location"), QStringLiteral("description")}; } // An Inform 6 identifier must begin with a letter or underscore; a name that // survives sanitisation as a leading digit (e.g. an object called "4 gallon // jug" -> "4gallonjug") is not a legal symbol and fails to compile. Prefix an // underscore so the symbol is legal without disturbing the printed short name. static QString ensureValidI6Identifier(QString name) { if (!name.isEmpty() && name.at(0).isDigit()) name = QLatin1Char('_') + name; return name; } QString Inform6Exporter::getExportName(const Room &room, std::optional suffix) { QString name = deaccent(stripUnaccentedCharacters(room.name)); name.remove(QLatin1Char(' ')); name.remove(QLatin1Char('-')); if (name.isEmpty()) name = QStringLiteral("room"); name = ensureValidI6Identifier(name); if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } QString Inform6Exporter::getExportName(const QString &displayName, std::optional suffix) { QString name = deaccent(stripUnaccentedCharacters(displayName)); name.remove(QLatin1Char(' ')); name.remove(QLatin1Char('-')); if (name.isEmpty()) name = QStringLiteral("item"); name = ensureValidI6Identifier(name); if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } // toI6Words needs deaccent; make it a member helper via a small local lambda in // the users below. Here we implement a file-local version taking a deaccenter. static QString toI6WordsImpl(const QString &text, const std::function &deacc) { const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts); QString output; for (const QString &word : words) { // A purely-numeric word (e.g. the "4" of "4 gallon jug") is not a legal // Inform 6 dictionary word -- it triggers a "'name' property should only // contain dictionary words" warning and is redundant, since I6 parses // numbers with its own token. Skip it. bool allDigits = true; for (const QChar qc : word) if (!qc.isDigit()) { allDigits = false; break; } if (allDigits) continue; if (!output.isEmpty()) output += QLatin1Char(' '); const QString w = deacc(word); // A single-character 'x' is read by Inform 6 as a ZSCII character // constant, not a dictionary word (it warns "'name' property should only // contain dictionary words"); the "//" suffix forces the dictionary-word // reading. if (w.length() == 1) output += SINGLE_QUOTE + w + QStringLiteral("//") + SINGLE_QUOTE; else output += toI6String(w, SINGLE_QUOTE); } if (output.isEmpty()) return toI6String(QStringLiteral("thing"), SINGLE_QUOTE); return output; } void Inform6Exporter::writeLocation(QString &out, Location *location) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; const QString cls = isRegionless(location->room->region) ? QStringLiteral("Object") : getExportName(location->room->region, std::nullopt); wl(cls + QStringLiteral(" ") + location->exportName + QLatin1Char(' ') + toI6String(location->room->name, DOUBLE_QUOTE)); wl(QStringLiteral(" with description")); wl(QStringLiteral(" ") + toI6String(location->room->description, DOUBLE_QUOTE) + QLatin1Char(',')); for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit != nullptr) wl(QStringLiteral(" ") + toI6PropertyName(direction) + QLatin1Char(' ') + (exit->hasDoor ? getExportName(exit->connectionName, std::nullopt) : exit->target->exportName) + QLatin1Char(',')); } wl(QStringLiteral(" has ") + (location->room->isDark ? QStringLiteral("~") : QString()) + QStringLiteral("light;")); wl(); } void Inform6Exporter::writeDoor(QString &out, Location *location, MappableDirection direction, Exit *exit) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; auto deacc = [this](const QString &s) { return deaccent(s); }; const MappableDirection opposite = oppositeOf(direction); wl(QStringLiteral("Object ") + getExportName(exit->connectionName, std::nullopt) + QLatin1Char(' ') + toI6String(exit->connectionDescription, DOUBLE_QUOTE)); wl(QStringLiteral(" with name ") + toI6WordsImpl(deaccent(stripUnaccentedCharacters(exit->connectionName)), deacc) + QLatin1Char(',')); wl(QStringLiteral(" description ") + toI6String(exit->connectionDescription, DOUBLE_QUOTE) + QLatin1Char(',')); wl(QStringLiteral(" found_in ") + location->exportName + QLatin1Char(' ') + exit->target->exportName + QLatin1Char(',')); wl(QStringLiteral(" door_to [; if (self in ") + location->exportName + QStringLiteral(") return ") + exit->target->exportName + QStringLiteral("; return ") + location->exportName + QStringLiteral(";],")); wl(QStringLiteral(" door_dir [; if (self in ") + location->exportName + QStringLiteral(") return ") + toI6PropertyName(direction) + QStringLiteral("; return ") + toI6PropertyName(opposite) + QStringLiteral("; ],")); wl(QStringLiteral(" has door ") + (exit->door.openable ? QStringLiteral("openable") : QString()) + QLatin1Char(' ') + (exit->door.open ? QStringLiteral("open") : QStringLiteral("~open")) + QLatin1Char(' ') + (exit->door.lockable ? QStringLiteral("lockable") : QString()) + QLatin1Char(' ') + (exit->door.locked ? QStringLiteral("locked") : QStringLiteral("~locked")) + QStringLiteral(" ;")); if (Exit *reciprocal = exit->target->bestExit(opposite)) reciprocal->exported = true; wl(); } static QStringList setAttributes(const Thing *thing) { QStringList attributes; if (!thing->contents.isEmpty()) { bool anyPartOf = false; for (const Thing *item : thing->contents) if (item->partOf) anyPartOf = true; if (anyPartOf) { attributes << QStringLiteral("transparent"); } else { attributes << QStringLiteral("open"); attributes << QStringLiteral("container"); } } if (thing->properNamed) attributes << QStringLiteral("proper"); if (thing->isPerson) { attributes << QStringLiteral("animate"); switch (thing->gender) { case Thing::Gender::Female: attributes << QStringLiteral("female"); break; case Thing::Gender::Male: attributes << QStringLiteral("male"); break; case Thing::Gender::Neuter: attributes << QStringLiteral("neuter"); break; } } if (thing->isScenery) attributes << QStringLiteral("scenery"); if (thing->isSupporter) attributes << QStringLiteral("supporter"); if (thing->isContainer && !attributes.contains(QStringLiteral("container"))) attributes << QStringLiteral("container"); if (thing->forceplural == Thing::Amounts::Plural) attributes << QStringLiteral("pluralname"); if (thing->worn) attributes << QStringLiteral("clothing"); return attributes; } static void writeOneThing(QString &out, const Thing *thing, int indent, const std::function &deacc) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; auto w = [&out](const QString &s) { out += s; }; wl(QStringLiteral("Object ") + QStringLiteral("-> ").repeated(indent) + QLatin1Char(' ') + thing->exportName + QLatin1Char(' ') + toI6String(stripUnaccentedCharacters(thing->displayName).trimmed(), DOUBLE_QUOTE)); wl(QStringLiteral(" with name ") + toI6WordsImpl(deacc(stripUnaccentedCharacters(thing->displayName)), deacc) + QLatin1Char(',')); w(QStringLiteral(" description ") + toI6String(thing->displayName, DOUBLE_QUOTE)); const QStringList attributes = setAttributes(thing); if (attributes.isEmpty()) { wl(QStringLiteral(";")); } else { wl(); wl(QStringLiteral(" has ") + attributes.join(QLatin1Char(' ')) + QStringLiteral(";")); } wl(); } static void exportThings(QString &out, const QList &things, const Thing *container, int indent, const std::function &deacc) { for (const Thing *thing : things) { if (thing->container != container) continue; writeOneThing(out, thing, indent, deacc); exportThings(out, thing->contents, thing, indent + 1, deacc); } } void Inform6Exporter::exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) { Q_UNUSED(history); auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; // Read this source as UTF-8 (short switches are honoured in a "!%" header // comment, which must be the file's first line) so accented characters in // names and descriptions compile directly rather than as mojibake. wl(QStringLiteral("!% -Cu")); wl(); wl(QStringLiteral("Constant Story ") + toI6String(title, DOUBLE_QUOTE) + QLatin1Char(';')); wl(QStringLiteral("Constant Headline ") + toI6String(QStringLiteral("^By ") + author + QLatin1Char('^') + description + QStringLiteral("^^"), DOUBLE_QUOTE) + QLatin1Char(';')); wl(); wl(QStringLiteral("Include \"Parser\";")); wl(QStringLiteral("Include \"VerbLib\";")); wl(); } void Inform6Exporter::exportContent(QString &out) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; auto deacc = [this](const QString &s) { return deaccent(s); }; if (!m_regionsInExportOrder.isEmpty()) { for (const ExportRegion *region : m_regionsInExportOrder) wl(QStringLiteral("Class ") + region->exportName + QLatin1Char(';')); wl(); } for (Location *location : m_locationsInExportOrder) { writeLocation(out, location); for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit == nullptr || !exit->hasDoor || exit->exported) continue; exit->exported = true; writeDoor(out, location, direction, exit); } exportThings(out, location->things, nullptr, 1, deacc); } wl(QStringLiteral("[ Initialise;")); if (!m_locationsInExportOrder.isEmpty()) { bool foundStart = false; for (Location *location : m_locationsInExportOrder) { if (!location->room->isStartRoom) continue; if (foundStart) { wl(QStringLiteral("! ") + location->exportName + QStringLiteral(" is a second start-room according to Trizbort.")); } else { wl(QStringLiteral(" location = ") + location->exportName + QLatin1Char(';')); foundStart = true; } } if (!foundStart) wl(QStringLiteral(" location = ") + m_locationsInExportOrder.first()->exportName + QLatin1Char(';')); } else { wl(QStringLiteral(" ! location = ...;")); } wl(QStringLiteral(" ! \"^^Your opening paragraph here...^^\";")); wl(QStringLiteral("];")); wl(); wl(QStringLiteral("Include \"Grammar\";")); wl(); if (!m_map.history.isEmpty()) { wl(QStringLiteral("Verb meta 'about' * -> About;")); wl(); wl(QStringLiteral("[ AboutSub ;")); wl(QStringLiteral(" print(") + toI6String(m_map.history, DOUBLE_QUOTE) + QStringLiteral(");")); wl(QStringLiteral("];")); wl(); } } } // namespace trizbort trizbort-qt-1.0.1/src/export/Inform6Exporter.h000066400000000000000000000062511526521612000213500ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_INFORM6EXPORTER_H #define TRIZBORT_EXPORT_INFORM6EXPORTER_H #include "CodeExporter.h" // Inform 6 exporter, ported from Inform6Exporter.cs. namespace trizbort { class Inform6Exporter : public CodeExporter { public: using CodeExporter::CodeExporter; protected: QStringList reservedWords() const override; void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) override; void exportContent(QString &out) override; QString getExportName(const Room &room, std::optional suffix) override; QString getExportName(const QString &displayName, std::optional suffix) override; private: void writeLocation(QString &out, Location *location); void writeDoor(QString &out, Location *location, MappableDirection direction, Exit *exit); }; } // namespace trizbort #endif // TRIZBORT_EXPORT_INFORM6EXPORTER_H trizbort-qt-1.0.1/src/export/Inform7Exporter.cpp000066400000000000000000000557111526521612000217110ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "Inform7Exporter.h" namespace trizbort { namespace { bool isNullOrWhitespace(const QString &s) { return s.trimmed().isEmpty(); } bool containsOddCharacters(const QString &text) { for (const QChar c : text) if (c != QLatin1Char(' ') && c != QLatin1Char('-') && !c.isLetterOrNumber()) return true; return false; } bool containsWord(const QString &text, const QStringList &words) { const QStringList found = text.split(QLatin1Char(' '), Qt::SkipEmptyParts); for (const QString &word : words) for (const QString &f : found) if (QString::compare(word, f, Qt::CaseInsensitive) == 0) return true; return false; } // stripOddCharacters(text, exceptChars): keep space, dash, alnum, exceptChars. QString stripOddCharacters(const QString &text, const QString &except = QString()) { QString out; for (const QChar c : text) if (c == QLatin1Char(' ') || c == QLatin1Char('-') || c.isLetterOrNumber() || except.contains(c)) out += c; return out.isEmpty() ? QStringLiteral("object") : out; } QString toInform7PrintableString(const QString &text) { QString r = text; r.replace(QLatin1String("'"), QLatin1String("[']")); r.replace(QLatin1String("\""), QLatin1String("'")); return QLatin1Char('"') + r + QLatin1Char('"'); } QString toInform7UnderstandString(const QString &text) { return QLatin1Char('"') + stripOddCharacters(text, QStringLiteral("'")) + QLatin1Char('"'); } QString toInform7UnderstandWords(const QString &text) { const QStringList words = text.split(QLatin1Char(' '), Qt::SkipEmptyParts); QString out; for (const QString &word : words) { if (!out.isEmpty()) out += QStringLiteral(" and "); out += toInform7UnderstandString(word); } return out; } QString getInform7Name(MappableDirection direction) { switch (direction) { case MappableDirection::North: return QStringLiteral("North"); case MappableDirection::South: return QStringLiteral("South"); case MappableDirection::East: return QStringLiteral("East"); case MappableDirection::West: return QStringLiteral("West"); case MappableDirection::NorthEast: return QStringLiteral("Northeast"); case MappableDirection::SouthEast: return QStringLiteral("Southeast"); case MappableDirection::NorthWest: return QStringLiteral("Northwest"); case MappableDirection::SouthWest: return QStringLiteral("Southwest"); case MappableDirection::Up: return QStringLiteral("Up"); case MappableDirection::Down: return QStringLiteral("Down"); case MappableDirection::In: return QStringLiteral("Inside"); case MappableDirection::Out: return QStringLiteral("Outside"); default: return QString(); } } // Lowercase direction name from the MappableDirection enum's own ToString (used // by the door writer): e.g. "north", "northeast". QString enumNameLower(MappableDirection d) { switch (d) { case MappableDirection::North: return QStringLiteral("north"); case MappableDirection::South: return QStringLiteral("south"); case MappableDirection::East: return QStringLiteral("east"); case MappableDirection::West: return QStringLiteral("west"); case MappableDirection::NorthEast: return QStringLiteral("northeast"); case MappableDirection::SouthEast: return QStringLiteral("southeast"); case MappableDirection::SouthWest: return QStringLiteral("southwest"); case MappableDirection::NorthWest: return QStringLiteral("northwest"); case MappableDirection::Up: return QStringLiteral("up"); case MappableDirection::Down: return QStringLiteral("down"); case MappableDirection::In: return QStringLiteral("in"); case MappableDirection::Out: return QStringLiteral("out"); default: return QStringLiteral("none"); } } bool isPlural(const QString &noun) { return !noun.isEmpty() && !noun.at(0).isUpper() && noun.endsWith(QLatin1Char('s')) && !noun.endsWith(QLatin1String("ss")); } QString genderName(Thing::Gender g) { switch (g) { case Thing::Gender::Neuter: return QStringLiteral("Neuter"); case Thing::Gender::Male: return QStringLiteral("Male"); case Thing::Gender::Female: return QStringLiteral("Female"); } return QString(); } QString getArticle(const Thing *thing) { const QString noun = thing->exportName; if (thing->properNamed) return QString(); if (noun.isEmpty() || isPlural(noun) || thing->forceplural == Thing::Amounts::Plural) { if (!noun.isEmpty() && noun.at(0).isUpper()) return QString(); return QStringLiteral("Some "); } if (QStringLiteral("aeiou").indexOf(noun.at(0).toLower()) >= 0) return QStringLiteral("An "); return QStringLiteral("A "); } QString whatItIs(const Thing *thing) { QString whatString; if (thing->isPerson) { whatString += thing->exportName + QStringLiteral(" is a ") + genderName(thing->gender) + QStringLiteral(" person"); } else if (thing->partOf) { whatString += QStringLiteral(" is "); } else if (thing->isScenery) { // These complete the "
" already emitted by the caller, // so they must be a predicate ("is scenery"), not a fresh sentence // (". is scenery") -- the latter leaves the "
" // dangling without a verb and the Inform 7 compiler rejects it. whatString += QStringLiteral("is scenery"); } else if (thing->isContainer) { whatString += QStringLiteral("is a container"); } else if (thing->isSupporter) { whatString += QStringLiteral("is a supporter"); } else { whatString = QStringLiteral("is a ") + (thing->forceplural == Thing::Amounts::Plural ? QStringLiteral("plural-named ") : QString()) + (thing->properNamed ? QStringLiteral("proper-named ") : QString()) + QStringLiteral("thing"); } return whatString; } // Inform 7 kind names. A whole object/room name equal to one of these clashes // with the kind ("A container is a thing" is rejected), and a determiner + kind // phrase ("No Region", "Every Room") is read as a quantified description rather // than a proper name. (thing/things/door/doors/object/objects are also in // reservedWords(), which already catches them as any word; listing them here too // is harmless and keeps the determiner + kind test complete.) QStringList inform7Kinds() { return {QStringLiteral("thing"), QStringLiteral("things"), QStringLiteral("object"), QStringLiteral("objects"), QStringLiteral("door"), QStringLiteral("doors"), QStringLiteral("room"), QStringLiteral("rooms"), QStringLiteral("region"), QStringLiteral("regions"), QStringLiteral("container"), QStringLiteral("containers"), QStringLiteral("supporter"), QStringLiteral("supporters"), QStringLiteral("person"), QStringLiteral("people"), QStringLiteral("man"), QStringLiteral("men"), QStringLiteral("woman"), QStringLiteral("women"), QStringLiteral("animal"), QStringLiteral("animals"), QStringLiteral("device"), QStringLiteral("devices"), QStringLiteral("vehicle"), QStringLiteral("vehicles"), QStringLiteral("backdrop"), QStringLiteral("backdrops"), QStringLiteral("scenery"), QStringLiteral("direction"), QStringLiteral("directions")}; } // Inform 7 movement directions. A whole name equal to one of these (e.g. "Up", // "North") clashes with the built-in direction and is rejected. QStringList inform7Directions() { return {QStringLiteral("north"), QStringLiteral("south"), QStringLiteral("east"), QStringLiteral("west"), QStringLiteral("northeast"), QStringLiteral("northwest"), QStringLiteral("southeast"), QStringLiteral("southwest"), QStringLiteral("up"), QStringLiteral("down"), QStringLiteral("inside"), QStringLiteral("outside")}; } // Determiners that, together with a kind word, make Inform 7 read a name as a // quantified description ("no regions", "every room") instead of a proper name. QStringList inform7Determiners() { return {QStringLiteral("no"), QStringLiteral("every"), QStringLiteral("all"), QStringLiteral("each"), QStringLiteral("both"), QStringLiteral("most"), QStringLiteral("any"), QStringLiteral("some")}; } // True if the whole (trimmed) name equals one of the words, case-insensitively. bool wholeNameIs(const QString &text, const QStringList &words) { const QString t = text.trimmed(); for (const QString &w : words) if (QString::compare(t, w, Qt::CaseInsensitive) == 0) return true; return false; } } // namespace QStringList Inform7Exporter::reservedWords() const { return {QStringLiteral("object"), QStringLiteral("objects"), QStringLiteral("thing"), QStringLiteral("things"), QStringLiteral("door"), QStringLiteral("doors"), QStringLiteral("is"), QStringLiteral("are"), QStringLiteral("in"), QStringLiteral("on"), QStringLiteral("and"), QStringLiteral("outside"), QStringLiteral("inside")}; } QString Inform7Exporter::inform7Name(const QString &nameIn, std::optional suffix) { QString name = nameIn; if (containsOddCharacters(name)) name = stripOddCharacters(name); // An Inform 7 name must not begin with a digit. In a value position -- e.g. // "Understand ... as " or "East of X is " -- a leading digit is // read as a number literal and the sentence is rejected by the compiler (an // object list entry like "\"15\" tile game" strips to "15 tile game"). Prefix // an underscore, which the compiler accepts; it is invisible to the player, // since a renamed thing carries its own printed name. if (!name.isEmpty() && name.at(0).isDigit()) name = QLatin1Char('_') + name; // Disambiguate names the Inform 7 compiler would otherwise reject, by // suffixing the internal identifier (the printed name is preserved // separately, so this stays invisible to the player). Three cases: // * a base reserved word appearing as any word of the name (existing); // * the whole name being exactly a kind ("container") or a direction // ("Up") -- but not merely ending in one, so "Dining Room" is untouched; // * a determiner together with a kind ("No Region", "Every Room"), which // Inform 7 reads as a quantified description. if (!suffix.has_value() && (containsWord(name, reservedWords()) || wholeNameIs(name, inform7Kinds()) || wholeNameIs(name, inform7Directions()) || (containsWord(name, inform7Determiners()) && containsWord(name, inform7Kinds())))) suffix = 1; if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } QString Inform7Exporter::getExportName(const Room &room, std::optional suffix) { return inform7Name(room.name, suffix); } QString Inform7Exporter::getExportName(const QString &displayName, std::optional suffix) { return inform7Name(displayName, suffix); } int Inform7Exporter::numberOfRoomsWithoutRegion() const { int n = 0; for (const Room &r : m_map.rooms) if (isRegionless(r.region)) ++n; return n; } QString Inform7Exporter::regionExportNameFor(const QString ®ionName) const { for (const ExportRegion *er : m_regionsInExportOrder) if (er->region.name == regionName) return er->exportName; return QString(); } bool Inform7Exporter::printThisLoc(QString &out, Location *location) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; auto w = [&out](const QString &s) { out += s; }; wl(QStringLiteral("part ") + location->exportName); wl(); w(QStringLiteral("There is a room called ") + location->exportName + QLatin1Char('.')); if (location->exportName != location->room->name) w(QStringLiteral(" The printed name of it is ") + toInform7PrintableString(location->room->name) + QLatin1Char('.')); const QString description = location->room->description; if (!description.isEmpty()) w(QLatin1Char(' ') + toInform7PrintableString(description) + (description.endsWith(QLatin1Char('.')) ? QString() : QStringLiteral("."))); if (location->room->isDark) w(QStringLiteral(" It is dark.")); if (!isRegionless(location->room->region)) wl(QStringLiteral(" It is in ") + regionExportNameFor(location->room->region) + QLatin1Char('.')); else wl(); wl(); // extra blank line if (location->room->isStartRoom) { w(QStringLiteral("The player is in ") + location->exportName + QLatin1Char('.')); wl(); } bool exportedThings = false; for (const Thing *thing : location->things) { exportedThings = true; QString thingText; if (!thing->isPerson) thingText += getArticle(thing) + thing->exportName + QLatin1Char(' '); thingText += whatItIs(thing); if (thing->container == nullptr) { thingText += QStringLiteral(" in ") + thing->location->exportName + QLatin1Char('.'); } else if (thing->container->isPerson) { thingText += (thing->worn ? QStringLiteral(" worn by ") : QStringLiteral(" carried by ")) + thing->container->exportName + QLatin1Char('.'); } else if (thing->partOf) { thingText += QStringLiteral(" part of ") + thing->container->exportName + QLatin1Char('.'); } else { thingText += QStringLiteral(" in ") + thing->container->exportName + QLatin1Char('.'); } w(thingText); if (thing->displayName != thing->exportName) w(QStringLiteral(" It is privately-named. The printed name of it is ") + toInform7PrintableString(thing->displayName) + (thing->displayName.endsWith(QLatin1Char('.')) ? QString() : QStringLiteral(".")) + QStringLiteral(" Understand ") + toInform7UnderstandWords(thing->displayName) + QStringLiteral(" as ") + thing->exportName + QLatin1Char('.')); wl(); if (!isNullOrWhitespace(thing->warningText)) wl(QStringLiteral("[Note: there were errors with your bracketed definitions.\n") + thing->warningText + QLatin1Char(']')); } if (exportedThings) wl(); bool exportedExits = false; for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit == nullptr || exit->exported) continue; exit->exported = true; exportedExits = true; if (!exit->hasDoor) { // writeNormalExit w(getInform7Name(direction) + QStringLiteral(" of ") + location->exportName + QStringLiteral(" is ") + exit->target->exportName + QLatin1Char('.')); const MappableDirection opposite = oppositeOf(direction); if (Exit::isReciprocated(location, direction, exit->target)) { if (Exit *reciprocal = exit->target->bestExit(opposite)) reciprocal->exported = true; } else if (exit->target->bestExit(opposite) == nullptr) { w(QLatin1Char(' ') + getInform7Name(opposite) + QStringLiteral(" of ") + exit->target->exportName + QStringLiteral(" is nowhere.")); } wl(); } else { // writeDoor const MappableDirection opposite = oppositeOf(direction); wl(exit->connectionName + QStringLiteral(" is a door. ") + exit->connectionName + QStringLiteral(" is ") + enumNameLower(direction) + QStringLiteral(" of ") + location->exportName + QStringLiteral(" and ") + enumNameLower(opposite) + QStringLiteral(" of ") + exit->target->exportName + QStringLiteral(". ")); wl(exit->connectionName + QStringLiteral(" is ") + (exit->door.open ? QStringLiteral("open") : QStringLiteral("closed")) + QStringLiteral(" and ") + (exit->door.openable ? QStringLiteral("openable") : QStringLiteral("not openable")) + QLatin1Char('.')); wl(exit->connectionName + QStringLiteral(" is ") + (exit->door.locked ? QStringLiteral("locked") : QStringLiteral("unlocked")) + QStringLiteral(" and ") + (exit->door.lockable ? QStringLiteral("lockable") : QStringLiteral("not lockable")) + QLatin1Char('.')); wl(QStringLiteral("The description of ") + exit->connectionName + QStringLiteral(" is ") + toInform7PrintableString(exit->connectionDescription) + QLatin1Char('.')); if (Exit *reciprocal = exit->target->bestExit(opposite)) reciprocal->exported = true; wl(); } } if (exportedExits) wl(); bool wroteConditionalExit = false; for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit != nullptr && exit->conditional) { wroteConditionalExit = true; wl(QStringLiteral("Instead of going ") + getInform7Name(direction).toLower() + QStringLiteral(" from ") + location->exportName + QStringLiteral(", block conditional exits.")); } } if (wroteConditionalExit) wl(); return wroteConditionalExit; } void Inform7Exporter::exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; wl(QLatin1Char('"') + title + QStringLiteral("\" by \"") + author + QLatin1Char('"')); wl(); if (!description.isEmpty()) { wl(QStringLiteral("The story description is ") + toInform7PrintableString(description) + (description.endsWith(QLatin1Char('.')) ? QString() : QStringLiteral("."))); wl(); } wl(QStringLiteral("Volume Trizbort generated map")); wl(); if (!isNullOrWhitespace(history)) { QString historyCoded = history; historyCoded.replace(QLatin1String("\r\n"), QLatin1String("\n")); historyCoded.replace(QLatin1String("\n"), QLatin1String("\n[line break]")); wl(QStringLiteral("chapter about")); wl(QStringLiteral("")); wl(QStringLiteral("abouting is an action out of world.")); wl(QStringLiteral("understand the command \"about\" as something new.")); wl(QStringLiteral("understand \"about\" as abouting.")); wl(QStringLiteral("carry out abouting: say \"") + historyCoded + QStringLiteral("\".")); wl(QStringLiteral("")); } } void Inform7Exporter::exportContent(QString &out) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; bool anyConditionalExits = false; if (numberOfRoomsWithoutRegion() > 0) { wl(QStringLiteral("book Regionless Rooms")); wl(); } for (Location *location : m_locationsInExportOrder) { if (!isRegionless(location->room->region)) continue; anyConditionalExits = printThisLoc(out, location) || anyConditionalExits; } for (const ExportRegion *region : m_regionsInExportOrder) { wl(QStringLiteral("book ") + getExportName(region->exportName, std::nullopt)); wl(); wl(QStringLiteral("There is a region called ") + getExportName(region->exportName, std::nullopt) + QLatin1Char('.')); wl(); for (Location *location : m_locationsInExportOrder) { if (location->room->region == region->region.name || location->room->region == region->exportName) anyConditionalExits = printThisLoc(out, location) || anyConditionalExits; } } if (anyConditionalExits) { wl(QStringLiteral("book conditional exit warning")); wl(); wl(QStringLiteral("To block conditional exits:")); wl(QStringLiteral("\tsay \"An export nymph appears on your keyboard. She says, 'You can't " "go that way, as that exit was marked as conditional, you know, a dotted " "line, in Trizbort. Obviously in your game you'll have a better rationale " "for this than, er, me.' She looks embarrassed. 'Bye!'\"")); wl(); } } } // namespace trizbort trizbort-qt-1.0.1/src/export/Inform7Exporter.h000066400000000000000000000064001526521612000213450ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_INFORM7EXPORTER_H #define TRIZBORT_EXPORT_INFORM7EXPORTER_H #include "CodeExporter.h" // Inform 7 exporter, ported from Inform7Exporter.cs. namespace trizbort { class Inform7Exporter : public CodeExporter { public: using CodeExporter::CodeExporter; protected: QStringList reservedWords() const override; void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) override; void exportContent(QString &out) override; QString getExportName(const Room &room, std::optional suffix) override; QString getExportName(const QString &displayName, std::optional suffix) override; private: QString inform7Name(const QString &name, std::optional suffix); bool printThisLoc(QString &out, Location *location); int numberOfRoomsWithoutRegion() const; QString regionExportNameFor(const QString ®ionName) const; }; } // namespace trizbort #endif // TRIZBORT_EXPORT_INFORM7EXPORTER_H trizbort-qt-1.0.1/src/export/QuestExporter.cpp000066400000000000000000000260641526521612000214700ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "QuestExporter.h" #include #include #include namespace trizbort { namespace { QString xmlText(const QString &text) { QString r = text; r.replace(QLatin1Char('&'), QLatin1String("&")); r.replace(QLatin1Char('<'), QLatin1String("<")); r.replace(QLatin1Char('>'), QLatin1String(">")); return r; } QString xmlAttr(const QString &text) { QString r = xmlText(text); r.replace(QLatin1Char('"'), QLatin1String(""")); return r; } QString stripOdd(const QString &text) { QString result; for (const QChar qc : text) { const ushort c = qc.unicode(); if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') result += qc; } return result; } QString questName(const QString &text, std::optional suffix) { QString name = stripOdd(text); if (name.isEmpty()) name = QStringLiteral("item"); if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } QString toQuestPropertyName(MappableDirection direction) { switch (direction) { case MappableDirection::North: return QStringLiteral("north"); case MappableDirection::South: return QStringLiteral("south"); case MappableDirection::East: return QStringLiteral("east"); case MappableDirection::West: return QStringLiteral("west"); case MappableDirection::NorthEast: return QStringLiteral("northeast"); case MappableDirection::NorthWest: return QStringLiteral("northwest"); case MappableDirection::SouthEast: return QStringLiteral("southeast"); case MappableDirection::SouthWest: return QStringLiteral("southwest"); case MappableDirection::Up: return QStringLiteral("up"); case MappableDirection::Down: return QStringLiteral("down"); case MappableDirection::In: return QStringLiteral("in"); case MappableDirection::Out: return QStringLiteral("out"); default: return QStringLiteral("north"); } } // Reproduces System.Drawing.ColorTranslator.ToHtml for the values Trizbort // stores. Room fill/border colors are loaded from the map file as #RRGGBB // hex (never named colors) or left at their default Color.Transparent. So an // unset (invalid) color renders as the named default "Transparent"; a set // color renders as uppercase "#RRGGBB". QString colorToHtml(const QColor &c) { if (!c.isValid()) return QStringLiteral("Transparent"); return QStringLiteral("#") + QStringLiteral("%1%2%3") .arg(c.red(), 2, 16, QLatin1Char('0')) .arg(c.green(), 2, 16, QLatin1Char('0')) .arg(c.blue(), 2, 16, QLatin1Char('0')) .toUpper(); } // Reproduces the C# expression `location.Room.Width / 32` formatted with the // default float ToString(): Width is a single-precision float and 32 an int, // so the division is done in float and printed with the shortest round-tripping // representation (e.g. "2", "4.375", "1.5"). std::to_chars gives the same // shortest form as .NET Core's float formatter. QString formatGrid(double dimension) { const float value = static_cast(dimension) / 32.0f; char buf[64]; const std::to_chars_result res = std::to_chars(buf, buf + sizeof(buf), value); return QString::fromLatin1(buf, static_cast(res.ptr - buf)); } } // namespace QStringList QuestExporter::reservedWords() const { return {QStringLiteral("object"), QStringLiteral("game"), QStringLiteral("turnscript")}; } QString QuestExporter::getExportName(const Room &room, std::optional suffix) { return questName(room.name, suffix); } QString QuestExporter::getExportName(const QString &displayName, std::optional suffix) { return questName(displayName, suffix); } void QuestExporter::exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) { Q_UNUSED(history); if (!includeWrapper()) return; auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; wl(QStringLiteral("")); wl(); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ") + QUuid::createUuid().toString(QUuid::WithoutBraces) + QStringLiteral("")); wl(QStringLiteral(" 1.0")); wl(QStringLiteral(" ") + QString::number(QDate::currentDate().year()) + QStringLiteral("")); wl(QStringLiteral(" ") + xmlText(author) + QStringLiteral("")); wl(QStringLiteral(" ") + xmlText(description) + QStringLiteral("")); wl(QStringLiteral(" ")); wl(); } void QuestExporter::exportContent(QString &out) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; for (Location *location : m_locationsInExportOrder) { const Room *room = location->room; wl(QStringLiteral(" exportName + QStringLiteral("\">")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ") + xmlText(room->name) + QStringLiteral("")); if (room->isDark) wl(QStringLiteral(" ")); wl(QStringLiteral(" ") + formatGrid(room->w) + QStringLiteral("")); wl(QStringLiteral(" ") + formatGrid(room->h) + QStringLiteral("")); wl(QStringLiteral(" ") + colorToHtml(room->fill) + QStringLiteral("")); wl(QStringLiteral(" ") + colorToHtml(room->border) + QStringLiteral("")); wl(QStringLiteral(" ") + xmlText(roomTooltipText(*room)) + QStringLiteral("")); if (!room->description.isEmpty()) wl(QStringLiteral(" ") + xmlText(room->description) + QStringLiteral("")); for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit != nullptr) { const QString dir = toQuestPropertyName(direction); wl(QStringLiteral(" target->exportName + QStringLiteral("\">")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); } } if (room->isStartRoom) { wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); } for (const Thing *thing : location->things) { wl(QStringLiteral(" exportName + QStringLiteral("\">")); wl(QStringLiteral(" ")); if (thing->isScenery) wl(QStringLiteral(" ")); if (thing->isContainer) { wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); } if (thing->forceplural == Thing::Amounts::Plural) wl(QStringLiteral(" ")); if (thing->isPerson) { if (thing->gender == Thing::Gender::Female) { if (thing->properNamed) wl(QStringLiteral(" ")); else wl(QStringLiteral(" ")); } else if (thing->gender == Thing::Gender::Male) { if (thing->properNamed) wl(QStringLiteral(" ")); else wl(QStringLiteral(" ")); } } wl(QStringLiteral(" ") + xmlText(thing->displayName) + QStringLiteral("")); wl(QStringLiteral(" ")); } wl(QStringLiteral(" ")); wl(); } wl(); if (includeWrapper()) { wl(QStringLiteral("")); wl(); } } } // namespace trizbort trizbort-qt-1.0.1/src/export/QuestExporter.h000066400000000000000000000071171526521612000211330ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_QUESTEXPORTER_H #define TRIZBORT_EXPORT_QUESTEXPORTER_H #include "CodeExporter.h" // Quest (Quest 5, .aslx) exporter, ported from QuestExporter.cs. Emits the // full ASL document: the QuestRoomsExporter room/object body wrapped in the // / header and closing . namespace trizbort { class QuestExporter : public CodeExporter { public: using CodeExporter::CodeExporter; protected: QStringList reservedWords() const override; void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) override; void exportContent(QString &out) override; QString getExportName(const Room &room, std::optional suffix) override; QString getExportName(const QString &displayName, std::optional suffix) override; // When false, the / wrapper (and closing ) are omitted, // producing just the room/object body (the C# QuestRoomsExporter). virtual bool includeWrapper() const { return true; } }; // Quest export without the file header: just the room/object bodies. class QuestRoomsExporter : public QuestExporter { public: using QuestExporter::QuestExporter; protected: bool includeWrapper() const override { return false; } }; } // namespace trizbort #endif // TRIZBORT_EXPORT_QUESTEXPORTER_H trizbort-qt-1.0.1/src/export/ZilExporter.cpp000066400000000000000000000317711526521612000211260ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "ZilExporter.h" namespace trizbort { namespace { bool isNullOrWhitespace(const QString &s) { return s.trimmed().isEmpty(); } bool startsWithVowel(const QString &s) { if (s.trimmed().isEmpty()) return false; const QChar c = s.at(0).toLower(); return c == QLatin1Char('a') || c == QLatin1Char('e') || c == QLatin1Char('i') || c == QLatin1Char('o') || c == QLatin1Char('u'); } bool containsOddCharacters(const QString &text) { for (const QChar c : text) { if (c != QLatin1Char(' ') && c != QLatin1Char('-') && !c.isLetterOrNumber()) return true; } return false; } bool containsWord(const QString &text, const QStringList &words) { const QStringList found = text.split(QLatin1Char(' '), Qt::SkipEmptyParts); for (const QString &word : words) for (const QString &f : found) if (QString::compare(word, f, Qt::CaseInsensitive) == 0) return true; return false; } QString stripOddCharacters(const QString &text) { QString out; for (const QChar c : text) { if (c == QLatin1Char(' ') || c == QLatin1Char('-') || c.isLetterOrNumber()) out += c; } return out.isEmpty() ? QStringLiteral("object") : out; } QString toZilString(const QString &str) { QString r = str; r.replace(QLatin1Char('\n'), QLatin1Char('|')); r.replace(QLatin1String("\""), QLatin1String("\\\"")); return QLatin1Char('"') + r + QLatin1Char('"'); } QString toZilPropertyName(MappableDirection direction) { switch (direction) { case MappableDirection::North: return QStringLiteral("NORTH"); case MappableDirection::South: return QStringLiteral("SOUTH"); case MappableDirection::East: return QStringLiteral("EAST"); case MappableDirection::West: return QStringLiteral("WEST"); case MappableDirection::NorthEast: return QStringLiteral("NE"); case MappableDirection::SouthEast: return QStringLiteral("SE"); case MappableDirection::SouthWest: return QStringLiteral("SW"); case MappableDirection::NorthWest: return QStringLiteral("NW"); case MappableDirection::Up: return QStringLiteral("UP"); case MappableDirection::Down: return QStringLiteral("DOWN"); case MappableDirection::In: return QStringLiteral("IN"); case MappableDirection::Out: return QStringLiteral("OUT"); default: return QString(); } } QStringList getObjectWords(const Thing *thing) { QStringList list; const QStringList words = thing->displayName.split(QLatin1Char(' ')); for (const QString &w : words) { const QString atom = stripOddCharacters(w).toUpper(); // A ZIL SYNONYM/ADJECTIVE value must be an atom: skip an empty result // (word was all punctuation) and a purely-numeric word such as the "15" // of "15 tile game", which ZIL parses as a number ("values ... must be // atoms"). if (atom.isEmpty()) continue; bool allDigits = true; for (const QChar qc : atom) if (!qc.isDigit()) { allDigits = false; break; } if (allDigits) continue; list.append(atom); } return list; } QString getFlags(const Thing *thing) { QString flags = QStringLiteral("TAKEBIT"); if (startsWithVowel(thing->displayName)) flags += QLatin1String(" VOWELBIT"); if (!thing->contents.isEmpty()) flags += QLatin1String(" CONTBIT"); return flags; } void exportThings(QString &out, const QList &things, const Thing *container) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; for (const Thing *thing : things) { if (thing->container != container) continue; wl(); wl(QStringLiteral("exportName); if (thing->container == nullptr) wl(QStringLiteral(" (IN ") + thing->location->exportName + QLatin1Char(')')); else wl(QStringLiteral(" (IN ") + thing->container->exportName + QLatin1Char(')')); wl(QStringLiteral(" (DESC ") + toZilString(thing->displayName) + QLatin1Char(')')); const QStringList words = getObjectWords(thing); if (words.size() > 0) wl(QStringLiteral(" (SYNONYM ") + words.at(words.size() - 1) + QLatin1Char(')')); if (words.size() > 1) wl(QStringLiteral(" (ADJECTIVE ") + words.mid(0, words.size() - 1).join(QLatin1Char(' ')) + QLatin1Char(')')); wl(QStringLiteral(" (FLAGS ") + getFlags(thing) + QStringLiteral(")>")); wl(); if (!thing->contents.isEmpty()) exportThings(out, thing->contents, thing); } } } // namespace QStringList ZilExporter::reservedWords() const { return {QStringLiteral("object"), QStringLiteral("objects")}; } QString ZilExporter::getExportName(const Room &room, std::optional suffix) { QString name = room.name.toUpper().replace(QLatin1Char(' '), QLatin1Char('-')); if (suffix.has_value() || containsWord(name, reservedWords()) || containsOddCharacters(name)) name = stripOddCharacters(name.replace(QLatin1Char(' '), QLatin1Char('-'))); if (suffix.has_value()) name = name + QLatin1Char('-') + QString::number(suffix.value()); return name; } QString ZilExporter::getExportName(const QString &displayName, std::optional suffix) { QString name = stripOddCharacters(displayName); name = name.toUpper().replace(QLatin1Char(' '), QLatin1Char('-')); if (name.isEmpty()) name = QStringLiteral("item"); if (suffix.has_value()) name = name + QString::number(suffix.value()); return name; } void ZilExporter::exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; // Determine the starting room. QString startingRoomName; const Room *startRoom = nullptr; for (const Room &r : m_map.rooms) { if (r.isStartRoom) { startRoom = &r; break; } } if (startRoom == nullptr) { if (!m_locationsInExportOrder.isEmpty()) startingRoomName = m_locationsInExportOrder.first()->exportName; } else { for (const Location *loc : m_locationsInExportOrder) { if (loc->room->id == startRoom->id) { startingRoomName = loc->exportName; break; } } } wl(QLatin1Char('"') + title + QStringLiteral(" main file\"")); wl(); wl(QStringLiteral("")); wl(QStringLiteral("")); wl(); wl(QStringLiteral("\"Main Loop\"")); wl(); wl(QStringLiteral("")); wl(); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ')); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" ")); wl(QStringLiteral(" >")); wl(QStringLiteral(" ,M-END>")); wl(QStringLiteral(" )>)>")); wl(QStringLiteral(" >>>")); wl(); wl(QStringLiteral("")); wl(); if (!isNullOrWhitespace(history)) { wl(QStringLiteral("")); wl(); wl(QStringLiteral(">")); wl(); } wl(QStringLiteral("\"Objects\"")); } void ZilExporter::exportContent(QString &out) { auto wl = [&out](const QString &s = QString()) { out += s; out += QLatin1Char('\n'); }; auto w = [&out](const QString &s) { out += s; }; bool needConditionalFunction = false; bool wroteConditionalFunction = false; for (Location *location : m_locationsInExportOrder) { wl(); wl(QStringLiteral("exportName); wl(QStringLiteral(" (DESC ") + toZilString(location->room->name) + QLatin1Char(')')); w(QStringLiteral(" (IN ROOMS)")); const QString primaryDesc = location->room->description; if (!isNullOrWhitespace(primaryDesc)) { wl(); w(QStringLiteral(" (LDESC ") + toZilString(primaryDesc) + QLatin1Char(')')); } for (MappableDirection direction : allDirections()) { Exit *exit = location->bestExit(direction); if (exit != nullptr && exit->conditional) { wl(); w(QStringLiteral(" (") + toZilPropertyName(direction) + QStringLiteral(" PER TRIZBORT-CONDITIONAL-EXIT)")); needConditionalFunction = true; } else if (exit != nullptr) { wl(); w(QStringLiteral(" (") + toZilPropertyName(direction) + QStringLiteral(" TO ") + exit->target->exportName + QLatin1Char(')')); if (!Exit::isReciprocated(location, direction, exit->target)) continue; Exit *reciprocal = exit->target->bestExit(oppositeOf(direction)); if (reciprocal != nullptr) reciprocal->exported = true; } } if (!location->room->isDark) { wl(); w(QStringLiteral(" (FLAGS LIGHTBIT)")); } wl(QStringLiteral(">")); wl(); if (needConditionalFunction && !wroteConditionalFunction) { wl(); wl(QStringLiteral("")); wl(QStringLiteral(" >")); wl(); wroteConditionalFunction = true; } exportThings(out, location->things, nullptr); } } } // namespace trizbort trizbort-qt-1.0.1/src/export/ZilExporter.h000066400000000000000000000060171526521612000205660ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef TRIZBORT_EXPORT_ZILEXPORTER_H #define TRIZBORT_EXPORT_ZILEXPORTER_H #include "CodeExporter.h" // ZIL (Infocom / ZILF) exporter, ported from Export/Languages/ZilExporter.cs. namespace trizbort { class ZilExporter : public CodeExporter { public: using CodeExporter::CodeExporter; protected: QStringList reservedWords() const override; void exportHeader(QString &out, const QString &title, const QString &author, const QString &description, const QString &history) override; void exportContent(QString &out) override; QString getExportName(const Room &room, std::optional suffix) override; QString getExportName(const QString &displayName, std::optional suffix) override; }; } // namespace trizbort #endif // TRIZBORT_EXPORT_ZILEXPORTER_H trizbort-qt-1.0.1/src/main.cpp000066400000000000000000001774231526521612000162470ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2026 Jason Self * SPDX-FileCopyrightText: 2010-2022 Genstein and Jason Lautzenheiser * SPDX-License-Identifier: GPL-3.0-or-later AND MIT * * Copyright (c) 2026 Jason Self * * This file is part of Trizbort-qt. * * Trizbort-qt 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 3 of the License, or (at your * option) any later version. * * Trizbort-qt 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 Trizbort-qt. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (c) 2010-2022 by Genstein and Jason Lautzenheiser. * * Trizbort, the Interactive Fiction Mapper. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ConnectionDialog.h" #include "ConnectionItem.h" #include "EditCommands.h" #include "MainWindow.h" #include "RoomDialog.h" #include "MapDocument.h" #include "MapRender.h" #include "AppSettings.h" #include "LabelGeometry.h" #include "MapScene.h" #include "MapStatisticsDialog.h" #include "RoomItem.h" #include "SettingsDialog.h" #include "TranscriptAutomapper.h" #include "TrizbortReader.h" #include "TrizbortWriter.h" #include "Version.h" #include "export/CodeExporter.h" #include "export/ExporterFactory.h" // Headless render: load a map and write a PNG or PDF, no window. Useful for CI // smoke tests and thumbnails. Run with QT_QPA_PLATFORM=offscreen on a headless // host. static int renderToFile(const QString &mapPath, const QString &outPath, bool pdf) { QTextStream err(stderr); trizbort::Map map; QString error; if (!trizbort::TrizbortReader::load(mapPath, map, &error)) { err << "load failed: " << error << Qt::endl; return 1; } const bool ok = pdf ? trizbort::renderMapToPdf(map, outPath, &error) : trizbort::renderMapToImage(map, outPath, &error); if (!ok) { err << error << Qt::endl; return 3; } QTextStream(stdout) << "rendered " << map.rooms.size() << " rooms, " << map.connections.size() << " connections -> " << outPath << Qt::endl; return 0; } using trizbort::makeExporter; // Headless export: load a map, run the named exporter, write the result. Used // to validate the C++ exporters against the golden fixtures. static int runExport(const QString &fmt, const QString &mapPath, const QString &outPath) { QTextStream err(stderr); trizbort::Map map; QString error; if (!trizbort::TrizbortReader::load(mapPath, map, &error)) { err << "load failed: " << error << Qt::endl; return 1; } auto exporter = makeExporter(fmt, map, mapPath); if (!exporter) { err << "unknown/unimplemented format: " << fmt << Qt::endl; return 2; } const QString text = exporter->exportToString(); if (!trizbort::writeExportText(outPath, text, &error)) { err << error << Qt::endl; return 3; } return 0; } // Headless transcript import: build a new map from a play transcript and save // it as a .trizbort file. static int runImportTranscript(const QString &transcriptPath, const QString &outPath) { QTextStream err(stderr); QFile file(transcriptPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err << "could not read " << transcriptPath << Qt::endl; return 1; } const QString text = QString::fromUtf8(file.readAll()); trizbort::Map map; map.regions.append(trizbort::Region{trizbort::kNoRegion, QColor(0, 0, 255), QColor(255, 255, 255), QString(), QString()}); trizbort::TranscriptAutomapper mapper; const int added = mapper.run(map, text); QString error; if (!trizbort::TrizbortWriter::save(outPath, map, &error)) { err << "save failed: " << error << Qt::endl; return 3; } QTextStream(stdout) << "imported transcript: " << added << " rooms, " << mapper.connectionsAdded() << " connections -> " << outPath << Qt::endl; return 0; } // Headless save: load a map and write it back out via the .trizbort writer. // Used to validate that a load/save round trip preserves the whole document. static int runSave(const QString &mapPath, const QString &outPath) { QTextStream err(stderr); trizbort::Map map; QString error; if (!trizbort::TrizbortReader::load(mapPath, map, &error)) { err << "load failed: " << error << Qt::endl; return 1; } if (!trizbort::TrizbortWriter::save(outPath, map, &error)) { err << "save failed: " << error << Qt::endl; return 3; } return 0; } // Quick-save (C# -q/--quicksave): re-save the map back to its own file, // upgrading/normalizing it in place. static int runQuickSave(const QString &mapPath) { return runSave(mapPath, mapPath); } // Smart-save (C# -s/--smartsave): re-save the project and, per the persisted // Smart Save preferences, write a PDF and/or an image alongside it. Headless, // so images render at 100%. static int runSmartSave(const QString &mapPath) { QTextStream err(stderr); trizbort::Map map; QString error; if (!trizbort::TrizbortReader::load(mapPath, map, &error)) { err << "load failed: " << error << Qt::endl; return 1; } if (!trizbort::TrizbortWriter::save(mapPath, map, &error)) { err << "save failed: " << error << Qt::endl; return 3; } QSettings s; const bool wantPdf = s.value(QStringLiteral("smartSave/pdf"), true).toBool(); const bool wantImage = s.value(QStringLiteral("smartSave/image"), true).toBool(); const QString fmt = s.value(QStringLiteral("smartSave/imageFormat"), QStringLiteral("png")).toString().toLower(); const QFileInfo fi(mapPath); const QString base = fi.path() + QLatin1Char('/') + fi.completeBaseName(); int rc = 0; if (wantPdf && !trizbort::renderMapToPdf(map, base + QStringLiteral(".pdf"), &error)) { err << "pdf failed: " << error << Qt::endl; rc = 3; } if (wantImage && !trizbort::renderMapToImage(map, base + QLatin1Char('.') + fmt, &error)) { err << "image failed: " << error << Qt::endl; rc = 3; } return rc; } // The most recent map on the recent-files list that still exists (C# // -a/--loadlastproject), or empty if there is none. static QString lastProjectPath() { const QStringList recent = QSettings().value(QStringLiteral("recentFiles")).toStringList(); for (const QString &p : recent) { if (!p.isEmpty() && QFile::exists(p)) return p; } return QString(); } static int runStats(const QString &mapPath) { trizbort::Map map; QString error; if (!trizbort::TrizbortReader::load(mapPath, map, &error)) { QTextStream(stderr) << "load failed: " << error << Qt::endl; return 1; } QTextStream(stdout) << trizbort::buildStatisticsReport(map) << Qt::endl; return 0; } // Headless self-test for connection-label placement geometry. Pins the // angle -> compass-port table (LabelGeometry.h) against the C# // CompassPoint.DirectionFromAngle oracle, including the negative-angle buckets // that depend on inRange() honouring high-to-low bounds. Prints PASS/FAIL. static int runGeometrySelftest() { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; // Build a delta whose resulting angleDeg is (approximately) the given // degrees, then read back the compass port. Since // angleDeg = -atan2(dy,dx)*180/pi, choosing phi = -deg (radians) yields // angleDeg == deg. auto compassAt = [](double deg) { const double phi = -deg * kPi / 180.0; double a = 0.0; return labelCompassFromDelta(QPointF(std::cos(phi), std::sin(phi)), a); }; // One representative angle per C# DirectionFromAngle bucket. The last two // are the high-to-low ([-90,-135] and [-135,-180]) buckets that regress to // the "e" default if inRange() ever drops its min>max swap. struct { double deg; const char *port; } kCases[] = { {20, "nw"}, {60, "se"}, {110, "sw"}, {160, "ne"}, {-20, "ne"}, {-60, "ne"}, {-110, "nw"}, {-160, "se"}, }; for (const auto &c : kCases) { const QString got = compassAt(c.deg); const QByteArray msg = QStringLiteral("angle %1 -> expected %2, got %3") .arg(c.deg) .arg(QLatin1String(c.port)) .arg(got) .toUtf8(); check(got == QLatin1String(c.port), msg.constData()); } // The eight buckets tile [-180,180], so no finite angle should fall through // to the "e" default. This is what actually fails if the negative-angle // swap is removed. int sweepFail = 0; for (int d = -179; d <= 179; ++d) if (compassAt(d) == QLatin1String("e")) ++sweepFail; check(sweepFail == 0, "no connection angle falls through to the East default"); // inRange() itself must accept a range written high-to-low. check(inRange(-110, -90, -135), "inRange honours high-to-low bounds"); check(!inRange(10, -90, -135), "inRange rejects values outside high-to-low bounds"); out << (failures == 0 ? "geometry-selftest: PASS" : "geometry-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } // Headless self-test for the GUI-independent editing API: build a small map, // round-trip it through the writer/reader, and export it. Prints PASS/FAIL. static int runEditSelftest() { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; Map map; const int a = map.addRoom(0, 0); const int b = map.addRoom(0, -128); check(map.rooms.size() == 2, "two rooms added"); check(a != b, "distinct room ids"); Room *ra = map.roomById(a); Room *rb = map.roomById(b); check(ra && rb, "rooms fetchable by id"); if (ra) { ra->name = QStringLiteral("Start Room"); ra->isStartRoom = true; ra->objectsText = QStringLiteral("a brass lantern"); } if (rb) rb->name = QStringLiteral("North Room"); const QString portA = Map::portFacing(*map.roomById(a), *map.roomById(b)); const QString portB = Map::portFacing(*map.roomById(b), *map.roomById(a)); check(portA == QLatin1String("n"), "portFacing north"); check(portB == QLatin1String("s"), "portFacing south"); map.addConnection(a, portA, b, portB); check(map.connections.size() == 1, "one connection added"); // Save, reload, and verify the model survived. const QString path = QDir(QDir::tempPath()).filePath(QStringLiteral("tz-edit-selftest.trizbort")); QString err; check(TrizbortWriter::save(path, map, &err), "save"); Map reloaded; check(TrizbortReader::load(path, reloaded, &err), "reload"); check(reloaded.rooms.size() == 2, "reloaded two rooms"); check(reloaded.connections.size() == 1, "reloaded one connection"); if (const Room *r = reloaded.roomById(a)) { check(r->name == QLatin1String("Start Room"), "reloaded room name"); check(r->isStartRoom, "reloaded start flag"); check(r->objectsText == QLatin1String("a brass lantern"), "reloaded objects"); } // The reloaded map must export. auto exporter = makeExporter(QStringLiteral("zil"), reloaded, path); const QString zil = exporter ? exporter->exportToString() : QString(); check(zil.contains(QLatin1String("START-ROOM")) || zil.contains(QLatin1String("ROOM")), "export produced rooms"); // Deleting the start room drops its connection too. reloaded.removeRoom(a); check(reloaded.rooms.size() == 1, "removeRoom left one room"); check(reloaded.connections.isEmpty(), "removeRoom dropped its connection"); // Facing-port geometry used by Insert Room on Connection: a room presents // the port on the side facing each neighbor, for any orientation. { Room mid; mid.x = 200; mid.y = 200; mid.w = 96; mid.h = 64; const QPointF c(mid.x + mid.w / 2.0, mid.y + mid.h / 2.0); check(MapScene::portTowards(mid, c + QPointF(300, 0)) == QLatin1String("e"), "facing port east"); check(MapScene::portTowards(mid, c - QPointF(300, 0)) == QLatin1String("w"), "facing port west"); check(MapScene::portTowards(mid, c + QPointF(0, 300)) == QLatin1String("s"), "facing port south"); check(MapScene::portTowards(mid, c - QPointF(0, 300)) == QLatin1String("n"), "facing port north"); check(MapScene::portTowards(mid, c + QPointF(300, 300)) == QLatin1String("se"), "facing port southeast"); } // Map statistics: connection/object/region breakdowns match the metrics. { Map sm; const int r0 = sm.addRoom(0, 0); const int r1 = sm.addRoom(0, -128); const int r2 = sm.addRoom(160, 0); if (Room *r = sm.roomById(r0)) { r->name = QStringLiteral("Cellar"); r->isStartRoom = true; r->isDark = true; r->objectsText = QStringLiteral("a coin\na key"); } if (Room *r = sm.roomById(r1)) r->name = QStringLiteral("Attic"); if (Room *r = sm.roomById(r2)) r->name = QStringLiteral("Hall"); sm.addConnection(r0, QStringLiteral("n"), r1, QStringLiteral("s")); sm.connections.last().startText = QStringLiteral("up"); sm.connections.last().endText = QStringLiteral("down"); sm.addConnection(r0, QStringLiteral("e"), r2, QStringLiteral("w")); const QString rep = buildStatisticsReport(sm); check(rep.contains(QLatin1String("# of Rooms: 3")), "stats room count"); check(rep.contains(QLatin1String("# of Dark Rooms: 1")), "stats dark count"); check(rep.contains(QLatin1String("Start room = Cellar")), "stats start room"); check(rep.contains(QLatin1String("1 up-down connection\n")), "stats up-down singular"); check(rep.contains(QLatin1String("Total # Rooms with Objects: 1, 0 with 1, 1 with 2")), "stats object breakdown"); check(rep.contains(QLatin1String("# of Connections: 2 total")), "stats connection total"); } // Snap-to-element magnetism: a room within the snap distance of a point. { Map nm; nm.addRoom(0, 0); // 96x64 at the origin MapScene sc; sc.setDocument(&nm); const int rid = nm.rooms.first().id; check(sc.roomNearestWithin(QPointF(10, 10), 16.0) == rid, "snap: point inside room"); check(sc.roomNearestWithin(QPointF(-8, 20), 16.0) == rid, "snap: point near room within range"); check(sc.roomNearestWithin(QPointF(-100, 20), 16.0) == -1, "snap: no room when far"); } // Port-adjust detail: the same direction snaps to 4, 8, or 16 compass points. { Room room; room.x = -48; room.y = -32; room.w = 96; room.h = 64; // center at the origin const QPointF ne(100, -60); AppSettings &appset = AppSettings::instance(); const int savedDetail = appset.portAdjustDetail; appset.portAdjustDetail = 16; check(MapScene::portTowards(room, ne) == QLatin1String("ene"), "port detail 16 -> ene"); appset.portAdjustDetail = 8; check(MapScene::portTowards(room, ne) == QLatin1String("ne"), "port detail 8 -> ne"); appset.portAdjustDetail = 4; check(MapScene::portTowards(room, ne) == QLatin1String("e"), "port detail 4 -> e"); appset.portAdjustDetail = savedDetail; } // Hand-drawn-global seeds a new room's hand-drawn flag. { AppSettings &appset = AppSettings::instance(); const bool savedHd = appset.handDrawnGlobal; appset.handDrawnGlobal = true; Map hm; const int rid = hm.addRoom(0, 0); check(hm.roomById(rid) && hm.roomById(rid)->handDrawn, "hand-drawn global seeds new rooms"); appset.handDrawnGlobal = false; const int rid2 = hm.addRoom(200, 0); check(hm.roomById(rid2) && !hm.roomById(rid2)->handDrawn, "new rooms are not hand-drawn when the default is off"); appset.handDrawnGlobal = savedHd; } // A legacy globally-hand-drawn map () with no per-room // handDrawn attributes loads with every room hand-drawn; an explicit per-room // value still wins over the legacy map-wide flag. { auto writeMap = [](const QString &name, const QString &xml) { const QString p = QDir(QDir::tempPath()).filePath(name); QFile f(p); if (f.open(QIODevice::WriteOnly)) { f.write(xml.toUtf8()); f.close(); } return p; }; const QString legacy = writeMap( QStringLiteral("tz-legacy-hd.trizbort"), QStringLiteral("" "" "" "yes" "")); Map lm; QString err; check(TrizbortReader::load(legacy, lm, &err) && lm.rooms.size() == 2, "legacy hand-drawn map loads"); bool allHd = !lm.rooms.isEmpty(); for (const Room &r : lm.rooms) if (!r.handDrawn) allHd = false; check(allHd, "legacy map-wide handDrawn applies to every room"); QFile::remove(legacy); const QString mixed = writeMap( QStringLiteral("tz-mixed-hd.trizbort"), QStringLiteral("" "" "yes" "")); Map mm; QString err2; check(TrizbortReader::load(mixed, mm, &err2) && mm.rooms.size() == 1 && !mm.rooms.first().handDrawn, "explicit per-room handDrawn overrides the legacy map-wide flag"); QFile::remove(mixed); } QFile::remove(path); out << (failures == 0 ? "edit-selftest: PASS" : "edit-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } // Scripted GUI pass over one on-disk map: loads it into a *real* MainWindow and // drives the same slots the menu items/keyboard invoke — add room, connect a // room, delete a selection — each followed by an undo/redo round-trip that must // return the model to where it started, then opens the real edit dialogs // (RoomDialog/ConnectionDialog/SettingsDialog) on live elements and checks they // read their inputs back. Returns the number of failed checks for this map (0 on // success). This is the "human clicking through the GUI" path exercised // end-to-end, as opposed to the scene-only smoke test below. A friend of // MainWindow so it can reach the private slots and the undo stack. namespace trizbort { int driveGuiOverMap(const QString &path) { QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL [" << path << "]: " << what << Qt::endl; ++failures; } }; // Probe the file first: a bad or unreadable path must fail a check (and make // the run exit non-zero), not block forever on loadFile's modal error dialog // under the offscreen platform. Once probed loadable, loadFile takes the // success path with no dialog. Map probe; QString probeErr; if (!TrizbortReader::load(path, probe, &probeErr)) { check(false, "map is loadable"); return failures; } MainWindow win; if (!win.loadFile(path)) { check(false, "MainWindow loads the map"); return failures; } Map &map = win.m_map; MapScene *scene = win.m_scene; win.m_undo.clear(); // 1. The scene mirrors the loaded model one item per element. int roomItems = 0; int connItems = 0; for (QGraphicsItem *it : scene->items()) { if (dynamic_cast(it)) ++roomItems; else if (dynamic_cast(it)) ++connItems; } check(roomItems == map.rooms.size(), "scene room-item count matches the model"); check(connItems == map.connections.size(), "scene connection-item count matches the model"); const qsizetype rooms0 = map.rooms.size(); const qsizetype conns0 = map.connections.size(); // 2. Add Room action, then undo/redo — the real slot and the real undo stack. win.addRoom(); check(map.rooms.size() == rooms0 + 1, "Add Room grows the model"); win.m_undo.undo(); check(map.rooms.size() == rooms0, "undo of Add Room restores the model"); win.m_undo.redo(); check(map.rooms.size() == rooms0 + 1, "redo re-adds the room"); win.m_undo.undo(); // back to the loaded baseline // 3. Connect-mode toggles without crashing. win.toggleConnectMode(true); win.toggleConnectMode(false); check(true, "connect-mode toggles without crashing"); // 4. On a map with rooms: grow a connected room off the first, undo it, and // round-trip that room through its edit dialog. if (rooms0 > 0) { const int firstId = map.rooms.first().id; scene->selectRoomItem(firstId); win.addConnectedRoom(QStringLiteral("e")); check(map.rooms.size() == rooms0 + 1 && map.connections.size() == conns0 + 1, "Add Connected Room adds a room and a connection"); win.m_undo.undo(); check(map.rooms.size() == rooms0 && map.connections.size() == conns0, "undo of Add Connected Room restores rooms and connections"); // The Room dialog, populated from a live room, reads its inputs back // (text and flag fields, which are unambiguous; colors/region are left to // the settings-dialog check to avoid color-button normalization noise). const Room &r0 = *map.roomById(firstId); RoomDialog rdlg(map, r0); const Room rr = rdlg.result(); check(rr.name == r0.name && rr.subtitle == r0.subtitle && rr.isDark == r0.isDark && rr.isStartRoom == r0.isStartRoom && rr.isEndRoom == r0.isEndRoom, "RoomDialog reads back an unedited room"); } // 5. The Connection dialog round-trips a live connection. if (conns0 > 0) { const Connection &c0 = map.connections.first(); ConnectionDialog cdlg(c0); const Connection cr = cdlg.result(); check(cr.style == c0.style && cr.flow == c0.flow && cr.name == c0.name && cr.startText == c0.startText && cr.midText == c0.midText && cr.endText == c0.endText, "ConnectionDialog reads back an unedited connection"); } // 6. The Settings dialog reads its inputs back on the loaded settings. { SettingsDialog sdlg(map.settings, map.regions); const MapSettings rs = sdlg.resultSettings(); check(rs.colors[ColorCanvas] == map.settings.colors[ColorCanvas] && rs.colors[ColorGrid] == map.settings.colors[ColorGrid] && rs.gridSize == map.settings.gridSize && rs.defaultRoomName == map.settings.defaultRoomName, "SettingsDialog reads back its inputs"); check(sdlg.resultRegions().size() == map.regions.size(), "SettingsDialog preserves the region count"); } // 7. Select-all then Delete empties the map; undo restores it whole. scene->selectAll(); win.deleteSelection(); check(map.rooms.isEmpty() && map.connections.isEmpty(), "Delete on select-all empties the map"); win.m_undo.undo(); check(map.rooms.size() == rooms0 && map.connections.size() == conns0, "undo restores the whole map after a select-all delete"); return failures; } } // namespace trizbort // Headless smoke test for the editing canvas: exercises the scene's item // machinery (add/move/connect/delete), confirms MainWindow loads a map, and — // when given map paths — drives the full scripted GUI pass over each of them. static int runGuiSelftest(const QStringList &samplePaths) { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; for (const QString &samplePath : samplePaths) failures += trizbort::driveGuiOverMap(samplePath); Map map; MapScene scene; scene.setDocument(&map); const int a = scene.addRoomAt(QPointF(0, 0)); const int b = scene.addRoomAt(QPointF(0, -128)); check(map.rooms.size() == 2 && a >= 0 && b >= 0, "addRoomAt created two rooms"); scene.roomMovedTo(a, QPointF(64, -64)); check(map.roomById(a) && map.roomById(a)->x == 64 && map.roomById(a)->y == -64, "roomMovedTo persisted to the model"); map.addConnection(a, QStringLiteral("n"), b, QStringLiteral("s")); scene.setDocument(&map); int roomItems = 0; int connItems = 0; for (QGraphicsItem *it : scene.items()) { if (dynamic_cast(it)) ++roomItems; else if (dynamic_cast(it)) ++connItems; } check(roomItems == 2, "scene has two room items"); check(connItems == 1, "scene has one connection item"); for (QGraphicsItem *it : scene.items()) { if (auto *ri = dynamic_cast(it)) { if (ri->roomId() == a) ri->setSelected(true); } } scene.deleteSelection(); check(map.rooms.size() == 1, "deleteSelection removed the room"); check(map.connections.isEmpty(), "deleteSelection removed its connection"); out << (failures == 0 ? "gui-selftest: PASS" : "gui-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } // Headless test for the canvas keyboard editing/navigation model and the port // helpers behind connect-mode docking ports. Drives the real key handlers. namespace trizbort { int runKeyboardSelftest() { QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; MainWindow win; Map &map = win.m_map; MapScene *scene = win.m_scene; map.clear(); const int a = map.addRoom(0, 0); // room A at the origin const int b = map.addRoom(0, -128); // room B one step north (y grows down) map.addConnection(a, QStringLiteral("n"), b, QStringLiteral("s")); scene->setDocument(&map); win.m_undo.clear(); auto sendKey = [&](int key, Qt::KeyboardModifiers mods) { QKeyEvent ev(QEvent::KeyPress, key, mods); win.handleCanvasKey(&ev); }; // Arrow key nudges the selected room by one grid step. scene->selectRoomItem(a); const double x0 = map.roomById(a)->x; sendKey(Qt::Key_Right, Qt::NoModifier); check(map.roomById(a) && map.roomById(a)->x == x0 + map.settings.gridSize, "arrow nudges the selected room by a grid step"); // Ctrl+Alt+arrow resizes the room. scene->selectRoomItem(a); const double w0 = map.roomById(a)->w; sendKey(Qt::Key_Right, Qt::ControlModifier | Qt::AltModifier); check(map.roomById(a)->w == w0 + map.settings.gridSize, "Ctrl+Alt+arrow resizes the room"); // Ctrl+arrow follows the connection to the room in that direction. scene->selectRoomItem(a); sendKey(Qt::Key_Up, Qt::ControlModifier); check(scene->selectedRoomId() == b, "Ctrl+arrow follows a connection to the next room"); // Ctrl+arrow with no connection that way adds a new connected room. const qsizetype roomsBefore = map.rooms.size(); scene->selectRoomItem(b); sendKey(Qt::Key_Up, Qt::ControlModifier); check(map.rooms.size() == roomsBefore + 1, "Ctrl+arrow adds a connected room when none exists"); // Port helpers behind connect-mode docking ports. check(MapScene::portTokensForDetail(4).size() == 4 && MapScene::portTokensForDetail(8).size() == 8 && MapScene::portTokensForDetail(16).size() == 16, "portTokensForDetail returns 4/8/16 ports"); if (const Room *ra = map.roomById(a)) { const QPointF north(ra->x + ra->w / 2.0, ra->y - 20); check(MapScene::nearestPort(*ra, north, 8) == QLatin1String("n"), "nearestPort picks the north port above the room"); } out << (failures == 0 ? "keyboard-selftest: PASS" : "keyboard-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } } // namespace trizbort // Headless test for the undo/redo command classes. static int runUndoSelftest() { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; Map map; MapScene scene; QUndoStack stack; scene.setUndoStack(&stack); scene.setDocument(&map); check(stack.isClean(), "starts clean"); const int a = scene.addRoomAt(QPointF(0, 0)); const int b = scene.addRoomAt(QPointF(0, -128)); check(map.rooms.size() == 2, "two rooms after add"); check(!stack.isClean(), "dirty after edits"); stack.push(new AddConnectionCommand(&scene, a, QStringLiteral("n"), b, QStringLiteral("s"))); check(map.connections.size() == 1, "connection added"); // Undo the connection, then redo it. stack.undo(); check(map.connections.isEmpty(), "undo removed connection"); stack.redo(); check(map.connections.size() == 1, "redo restored connection"); // Edit a room name, then undo. { Room before = *map.roomById(a); Room after = before; after.name = QStringLiteral("Renamed"); stack.push(new EditRoomCommand(&scene, a, before, after)); check(map.roomById(a)->name == QLatin1String("Renamed"), "room renamed"); stack.undo(); check(map.roomById(a)->name == before.name, "undo restored room name"); } // Move a room, then undo. { const QPointF oldPos(map.roomById(a)->x, map.roomById(a)->y); QList moves{{a, oldPos, QPointF(96, 96)}}; stack.push(new MoveRoomsCommand(&scene, moves)); check(map.roomById(a)->x == 96 && map.roomById(a)->y == 96, "room moved"); stack.undo(); check(map.roomById(a)->x == oldPos.x() && map.roomById(a)->y == oldPos.y(), "undo restored room position"); } // Delete a room (and its connection), then undo to restore both. { const qsizetype roomsBefore = map.rooms.size(); const qsizetype connsBefore = map.connections.size(); stack.push(new DeleteElementsCommand(&scene, QList{a}, QList{})); check(map.rooms.size() == roomsBefore - 1, "delete removed the room"); check(map.connections.isEmpty(), "delete removed its connection"); stack.undo(); check(map.rooms.size() == roomsBefore, "undo restored the room"); check(map.connections.size() == connsBefore, "undo restored its connection"); } // Clean-state tracking round-trips. stack.setClean(); check(stack.isClean(), "clean after setClean"); stack.undo(); check(!stack.isClean(), "dirty after undoing past the clean point"); out << (failures == 0 ? "undo-selftest: PASS" : "undo-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } // Headless test for the automap primitive: create connected rooms in each // direction and check placement and the resulting connection. static int runAutomapSelftest() { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; struct Case { const char *dir; double x; double y; }; // From a 96x64 room at (0,0) with a 64-unit gap: north/west subtract the new // room size + gap; east/south add the source size + gap; all snapped. const Case cases[] = { {"n", 0, -128}, {"s", 0, 128}, {"e", 160, 0}, {"w", -160, 0}, {"ne", 160, -128}, {"sw", -160, 128}, }; for (const Case &c : cases) { Map map; MapScene scene; QUndoStack stack; scene.setUndoStack(&stack); scene.setDocument(&map); const int a = scene.addRoomAt(QPointF(0, 0)); scene.selectRoomItem(a); auto *cmd = new AddConnectedRoomCommand(&scene, a, QString::fromLatin1(c.dir)); check(cmd->valid(), "command valid"); const int nid = cmd->newRoomId(); stack.push(cmd); check(map.rooms.size() == 2, "room added"); check(map.connections.size() == 1, "connection added"); const Room *nr = map.roomById(nid); check(nr && nr->x == c.x && nr->y == c.y, c.dir); if (!(nr && nr->x == c.x && nr->y == c.y)) out << " (" << c.dir << " got " << (nr ? nr->x : 0) << "," << (nr ? nr->y : 0) << ")" << Qt::endl; // The connection docks the source's direction port to the new room. const Connection &conn = map.connections.first(); check(conn.vertices.size() == 2 && conn.vertices.at(0).roomId == a && conn.vertices.at(0).port == QLatin1String(c.dir), "connection docks source port"); stack.undo(); check(map.rooms.size() == 1 && map.connections.isEmpty(), "undo removes both"); } out << (failures == 0 ? "automap-selftest: PASS" : "automap-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } // Headless test for the transcript automapper: a short walk builds a 2x2 grid. static int runTranscriptSelftest() { using namespace trizbort; QTextStream out(stdout); int failures = 0; auto check = [&](bool ok, const char *what) { if (!ok) { out << " FAIL: " << what << Qt::endl; ++failures; } }; auto roomNamed = [](const Map &m, const QString &n) -> const Room * { for (const Room &r : m.rooms) if (r.name == n) return &r; return nullptr; }; auto roomIdNamed = [&](const Map &m, const QString &n) { const Room *r = roomNamed(m, n); return r ? r->id : -1; }; auto isReal = [](const Connection &c) { return c.vertices.size() >= 2 && c.vertices.at(0).docked && c.vertices.at(1).docked; }; auto realConnections = [&](const Map &m) { int count = 0; for (const Connection &c : m.connections) if (isReal(c)) ++count; return count; }; auto connectionBetween = [&](const Map &m, int a, int b) -> const Connection * { for (const Connection &c : m.connections) { if (!isReal(c)) continue; const int r0 = c.vertices.at(0).roomId; const int r1 = c.vertices.at(1).roomId; if ((r0 == a && r1 == b) || (r0 == b && r1 == a)) return &c; } return nullptr; }; // A dangling exit stub: one vertex docked to the room at `port`, one free. auto stubsFrom = [](const Map &m, int roomId, const QString &port) { int count = 0; for (const Connection &c : m.connections) { bool dockHere = false; bool hasFree = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId && v.port == port) dockHere = true; if (!v.docked) hasFree = true; } if (dockHere && hasFree) ++count; } return count; }; auto stubText = [](const Map &m, int roomId, const QString &port) { for (const Connection &c : m.connections) { bool dockHere = false; bool hasFree = false; for (const Vertex &v : c.vertices) { if (v.docked && v.roomId == roomId && v.port == port) dockHere = true; if (!v.docked) hasFree = true; } if (dockHere && hasFree) return c.startText; } return QString(); }; auto runWith = [](const QString &t, const AutomapSettings &s, AutomapController *c) { Map m; TranscriptAutomapper mp(s); mp.begin(m, c); QString text = t; mp.feed(text.split(QLatin1Char('\n')), true); return m; }; const QString transcript = QStringLiteral( "Cave Adventure\n" "An interactive fiction\n" "\n" "West of House\n" "You are standing in an open field west of a white house.\n" "\n" ">north\n" "North of House\n" "You are facing the north side of a white house.\n" "\n" ">east\n" "Behind House\n" "You are behind the white house.\n" "\n" ">south\n" "South of House\n" "This is the south side of a white house.\n" "\n" ">tb see brass lantern\n" ">\n"); Map map; TranscriptAutomapper mapper; mapper.run(map, transcript); check(map.rooms.size() == 4, "four rooms created"); check(realConnections(map) == 3, "three connections created"); const Room *west = roomNamed(map, QStringLiteral("West of House")); const Room *north = roomNamed(map, QStringLiteral("North of House")); const Room *behind = roomNamed(map, QStringLiteral("Behind House")); const Room *south = roomNamed(map, QStringLiteral("South of House")); check(west && north && behind && south, "all four rooms present by name"); check(west && west->isStartRoom, "first room is the start room"); if (west && north) check(north->x == west->x && north->y == west->y - 128, "north placed above west"); if (behind && north) check(behind->y == north->y && behind->x == north->x + 160, "behind placed east of north"); // The 'tb see' command adds an object to the last room (South of House). check(south && south->objectsText.contains(QLatin1String("brass lantern")), "tb see added an object"); check(west && stubsFrom(map, west->id, QStringLiteral("w")) == 1, "default guessExits stubbed the west exit"); check(west && stubsFrom(map, west->id, QStringLiteral("n")) == 0, "the north stub was removed by the move north"); // Re-exports cleanly. auto exporter = makeExporter(QStringLiteral("inform7"), map, QStringLiteral("t.trizbort")); check(exporter && exporter->exportToString().contains(QLatin1String("West of House")), "automapped map exports"); { const QString t = QStringLiteral( "Stairs Game\n" "\n" "Cellar\n" "A damp cellar. A ladder lets you ascend.\n" "\n" ">up\n" "Kitchen\n" "A warm kitchen.\n" "\n" ">down\n" "Cellar\n" "A damp cellar. A ladder lets you ascend.\n" "\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); check(m.rooms.size() == 2, "up test: two rooms"); check(realConnections(m) == 1, "up test: one real connection after going back down"); const int cellar = roomIdNamed(m, QStringLiteral("Cellar")); const int kitchen = roomIdNamed(m, QStringLiteral("Kitchen")); const Connection *c = connectionBetween(m, cellar, kitchen); check(c && c->vertices.at(0).roomId == cellar && c->vertices.at(0).port == QLatin1String("nnw"), "up docks the source at nnw"); check(c && c->vertices.at(1).roomId == kitchen && c->vertices.at(1).port == QLatin1String("ssw"), "up docks the target at ssw"); check(c && c->startText == QLatin1String("up") && c->endText == QLatin1String("down"), "up sets both labels"); const Room *cr = m.roomById(cellar); const Room *kr = m.roomById(kitchen); check(cr && kr && kr->x == cr->x && kr->y == cr->y - 128, "up places the room above"); check(stubsFrom(m, cellar, QStringLiteral("nnw")) == 0, "the guessed up stub was superseded"); auto ex = makeExporter(QStringLiteral("inform7"), m, QStringLiteral("t.trizbort")); const QString ni = ex ? ex->exportToString() : QString(); check(ni.contains(QLatin1String("Up of Cellar is Kitchen")), "up exports as up in Inform 7"); check(!ni.contains(QLatin1String("North of Cellar")), "up does not export as north"); } { const QString t = QStringLiteral( "Hatch Game\n" "\n" "Attic\n" "Dusty.\n" "\n" ">d\n" "Landing\n" "Bare.\n" "\n" ">in\n" "Closet\n" "Dark.\n" "\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); const int attic = roomIdNamed(m, QStringLiteral("Attic")); const int landing = roomIdNamed(m, QStringLiteral("Landing")); const int closet = roomIdNamed(m, QStringLiteral("Closet")); const Connection *d = connectionBetween(m, attic, landing); check(d && d->vertices.at(0).port == QLatin1String("ssw") && d->vertices.at(1).port == QLatin1String("nnw"), "down docks ssw to nnw"); check(d && d->startText == QLatin1String("down") && d->endText == QLatin1String("up"), "down labels down/up"); const Connection *i = connectionBetween(m, landing, closet); check(i && i->vertices.at(0).port == QLatin1String("ene") && i->vertices.at(1).port == QLatin1String("wnw"), "in docks ene to wnw"); check(i && i->startText == QLatin1String("in") && i->endText == QLatin1String("out"), "in labels in/out"); const Room *lr = m.roomById(landing); const Room *ar = m.roomById(attic); check(lr && ar && lr->y == ar->y + 128, "down places the room below"); } { const QString t = QStringLiteral( "One Way Game\n" "\n" "Top\n" "High.\n" "\n" ">down\n" "Bottom\n" "Low.\n" "\n" ">up\n" "Top\n" "High.\n" "\n" ">\n"); AutomapSettings s; s.assumeTwoWayConnections = false; Map m = runWith(t, s, nullptr); check(realConnections(m) == 1, "one-way test: a single connection"); const Connection *c = connectionBetween(m, roomIdNamed(m, QStringLiteral("Top")), roomIdNamed(m, QStringLiteral("Bottom"))); check(c && c->flow == ConnectionFlow::TwoWay, "traversing back upgrades one-way to two-way"); check(c && c->startText == QLatin1String("down") && c->endText == QLatin1String("up"), "labels stay relative to the first traversal"); } { const QString t = QStringLiteral( "Loop Game\n" "\n" "Alcove\n" "Room a.\n" "\n" ">north\n" "Bower\n" "Room b.\n" "\n" ">south\n" "Alcove\n" "Room a.\n" "\n" ">north\n" "Bower\n" "Room b.\n" "\n" ">northeast\n" "Alcove\n" "Room a.\n" "\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); check(m.rooms.size() == 2, "loop test: two rooms"); check(realConnections(m) == 1, "loop test: one connection reused"); const Connection *c = connectionBetween(m, roomIdNamed(m, QStringLiteral("Alcove")), roomIdNamed(m, QStringLiteral("Bower"))); check(c && c->vertices.at(1).port == QLatin1String("ne"), "a different direction re-docks the traversed end"); } // 'tb exit ' adds a dangling stub; 'tb noexit ' takes it away. { const QString t = QStringLiteral( "Stub Game\n" "\n" "Kitchen\n" "A cozy kitchen.\n" "\n" ">tb exit north\n" ">tb exit east\n" ">tb exit up\n" ">tb exit down\n" ">tb noexit north\n" ">tb noexit down\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); check(m.rooms.size() == 1, "stub test: one room"); const int kitchen = m.rooms.isEmpty() ? -1 : m.rooms.first().id; check(kitchen >= 0 && stubsFrom(m, kitchen, QStringLiteral("n")) == 0, "tb noexit removed the north stub"); check(kitchen >= 0 && stubsFrom(m, kitchen, QStringLiteral("e")) == 1, "tb exit east stub remains"); check(kitchen >= 0 && stubsFrom(m, kitchen, QStringLiteral("nnw")) == 1 && stubText(m, kitchen, QStringLiteral("nnw")) == QLatin1String("up"), "tb exit up stubs nnw with an up label"); check(kitchen >= 0 && stubsFrom(m, kitchen, QStringLiteral("ssw")) == 0, "tb noexit down removed the ssw stub"); } { const QString t = QStringLiteral( "Blocked Game\n" "\n" "Clearing\n" "A path leads north.\n" "\n" ">north\n" "You can't go that way.\n" "\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); const int clearing = m.rooms.isEmpty() ? -1 : m.rooms.first().id; check(m.rooms.size() == 1 && stubsFrom(m, clearing, QStringLiteral("n")) == 0, "a failed move still removes the stub"); } // 'trypush ' nudges the current room a step in that direction. { const QString t = QStringLiteral( "Push Game\n" "\n" "Hall\n" "A long hall.\n" "\n" ">trypush east\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); check(m.rooms.size() == 1, "trypush test: one room"); if (!m.rooms.isEmpty()) check(m.rooms.first().x > 100.0, "trypush east moved the room right"); } // guessExits: direction words in a description become dangling stubs. { const QString t = QStringLiteral( "Guess Game\n" "\n" "Deck\n" "The bow lies fore and the stern aft; a ladder lets you ascend and a hatch leads below. Nothing lies to starboard.\n" "\n" ">port\n" "Rail\n" "The deck lies to starboard. The sea stretches in all directions.\n" "\n" ">tb noexit north\n" ">look\n" "Rail\n" "The deck lies to starboard. The sea stretches in all directions.\n" "\n" ">\n"); Map m = runWith(t, AutomapSettings(), nullptr); check(m.rooms.size() == 2, "guess test: two rooms"); const int deck = roomIdNamed(m, QStringLiteral("Deck")); const int rail = roomIdNamed(m, QStringLiteral("Rail")); check(stubsFrom(m, deck, QStringLiteral("n")) == 1, "guessed fore as north"); check(stubsFrom(m, deck, QStringLiteral("s")) == 1, "guessed aft as south"); check(stubsFrom(m, deck, QStringLiteral("e")) == 1, "guessed starboard as east"); check(stubsFrom(m, deck, QStringLiteral("nnw")) == 1 && stubText(m, deck, QStringLiteral("nnw")) == QLatin1String("up"), "guessed ascend as a labeled up stub"); check(stubsFrom(m, deck, QStringLiteral("ssw")) == 1 && stubText(m, deck, QStringLiteral("ssw")) == QLatin1String("down"), "guessed below as a labeled down stub"); check(stubsFrom(m, deck, QStringLiteral("w")) == 0, "the port move superseded its stub"); check(connectionBetween(m, deck, rail) != nullptr, "port moved west to Rail"); check(stubsFrom(m, rail, QStringLiteral("e")) == 0, "no stub where a real connection is docked"); check(stubsFrom(m, rail, QStringLiteral("n")) == 0, "a removed stub is not guessed again on a revisit"); int railStubs = 0; for (const char *p : {"s", "w", "ne", "nw", "se", "sw"}) railStubs += stubsFrom(m, rail, QLatin1String(p)); check(railStubs == 6, "all directions stubs the remaining compass points"); AutomapSettings off; off.guessExits = false; Map m2 = runWith(t, off, nullptr); int any = 0; for (const Connection &c : m2.connections) if (!isReal(c)) ++any; check(any == 0, "guessExits=false adds no stubs"); } { struct Recorder : AutomapController { int asks = 0; int answer = -2; bool stop = false; QList last; int disambiguateRoom(const RoomQuery &query, const QList &candidates, bool *stopAsking) override { Q_UNUSED(query); ++asks; last = candidates; if (stop) *stopAsking = true; return answer == -2 ? candidates.first().id : answer; } }; const QString t = QStringLiteral( "Twin Game\n" "\n" "Hall\n" "A grand hall.\n" "\n" ">north\n" "Hall\n" "A dim hall.\n" "\n" ">north\n" "Hall\n" "A dim hall.\n" "\n" ">\n"); Recorder newRoom; newRoom.answer = -1; Map m = runWith(t, AutomapSettings(), &newRoom); check(newRoom.asks == 1, "differing description asks once"); check(m.rooms.size() == 2, "answering new room creates a second Hall"); check(newRoom.last.size() == 1 && newRoom.last.first().description == QLatin1String("A grand hall."), "the candidate carries its description"); Recorder same; Map m2 = runWith(t, AutomapSettings(), &same); check(same.asks == 1 && m2.rooms.size() == 1, "a description accepted as the same room is remembered"); check(m2.rooms.first().description == QLatin1String("A grand hall."), "the first description stays primary"); Recorder stopper; stopper.stop = true; const QString t3 = t + QStringLiteral( ">north\n" "Hall\n" "A third hall.\n" "\n" ">\n"); Map m3 = runWith(t3, AutomapSettings(), &stopper); check(stopper.asks == 1 && m3.rooms.size() == 1, "auto-choose the rest stops asking"); Recorder assume; AutomapSettings sameName; sameName.assumeSameNameSameRoom = true; Map m4 = runWith(t, sameName, &assume); check(assume.asks == 0 && m4.rooms.size() == 1, "assumeSameNameSameRoom never asks"); Recorder terse; AutomapSettings brief; brief.verboseTranscript = false; Map m5 = runWith(t, brief, &terse); check(terse.asks == 0 && m5.rooms.size() == 1, "a terse transcript never asks"); Recorder revisit; const QString t6 = QStringLiteral( "Revisit Game\n" "\n" "Hall\n" "A grand hall.\n" "\n" ">north\n" "Study\n" "Books.\n" "\n" ">south\n" "Hall\n" "A grand hall.\n" "\n" ">north\n" "Study\n" "\n" ">\n"); Map m6 = runWith(t6, AutomapSettings(), &revisit); check(revisit.asks == 0 && m6.rooms.size() == 2, "matching or missing descriptions do not ask"); Map m7 = runWith(t, AutomapSettings(), nullptr); check(m7.rooms.size() == 1, "the non-interactive default auto-chooses the first room"); } { struct Chooser : AutomapController { SameDir choice = SameDir::KeepBoth; int calls = 0; QString newName; SameDir sameDirection(int, int, const QString &, const QString &name) override { ++calls; newName = name; return choice; } }; const QString t = QStringLiteral( "Fork Game\n" "\n" "Start\n" "Begin here.\n" "\n" ">north\n" "Meadow\n" "Grass.\n" "\n" ">south\n" "Start\n" "Begin here.\n" "\n" ">north\n" "Forest\n" "Trees.\n" "\n" ">\n"); Chooser keepExisting; keepExisting.choice = AutomapController::SameDir::KeepExisting; Map m = runWith(t, AutomapSettings(), &keepExisting); check(keepExisting.calls == 1 && keepExisting.newName == QLatin1String("Forest"), "same-direction conflict asks once with the new name"); check(m.rooms.size() == 2 && !roomNamed(m, QStringLiteral("Forest")), "keep existing adds nothing"); Chooser keepNew; keepNew.choice = AutomapController::SameDir::KeepNew; Map m2 = runWith(t, AutomapSettings(), &keepNew); check(m2.rooms.size() == 2 && !roomNamed(m2, QStringLiteral("Meadow")), "keep new removes the existing room"); check(connectionBetween(m2, roomIdNamed(m2, QStringLiteral("Start")), roomIdNamed(m2, QStringLiteral("Forest"))) != nullptr, "keep new connects the new room"); Chooser keepBoth; Map m3 = runWith(t, AutomapSettings(), &keepBoth); check(m3.rooms.size() == 3 && realConnections(m3) == 2, "keep both keeps both rooms"); const QString t2 = QStringLiteral( "Straight Game\n" "\n" "Start\n" "Begin here.\n" "\n" ">north\n" "Meadow\n" "Grass.\n" "\n" ">north\n" "Forest\n" "Trees.\n" "\n" ">\n"); Chooser quiet; Map m4 = runWith(t2, AutomapSettings(), &quiet); check(quiet.calls == 0 && m4.rooms.size() == 3, "no conflict when the direction is free"); } out << (failures == 0 ? "transcript-selftest: PASS" : "transcript-selftest: FAIL") << Qt::endl; return failures == 0 ? 0 : 1; } int main(int argc, char *argv[]) { QApplication app(argc, argv); QApplication::setApplicationName(QStringLiteral("Trizbort-qt")); QApplication::setOrganizationName(QStringLiteral("Trizbort")); QApplication::setApplicationVersion(QStringLiteral(TRIZBORT_VERSION)); // Application icon for windows, the taskbar/dock and the Alt-Tab switcher. // The SVG source is embedded via packaging/app_icon.qrc; Qt's SVG icon engine // (Qt6::Svg) renders it on demand at whatever size each surface requests, so // one vector file replaces a ladder of pre-rasterised PNGs. { QIcon appIcon(QStringLiteral(":/icons/trizbort-qt.svg")); if (!appIcon.isNull()) QApplication::setWindowIcon(appIcon); } if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--geometry-selftest")) return runGeometrySelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--edit-selftest")) return runEditSelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--transcript-selftest")) return runTranscriptSelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--automap-selftest")) return runAutomapSelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--undo-selftest")) return runUndoSelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--keyboard-selftest")) return trizbort::runKeyboardSelftest(); if (argc >= 2 && QString::fromLocal8Bit(argv[1]) == QLatin1String("--gui-selftest")) { QStringList guiMaps; for (int i = 2; i < argc; ++i) guiMaps << QString::fromLocal8Bit(argv[i]); return runGuiSelftest(guiMaps); } QString mapPath; QString renderPath; QString pdfPath; QString savePath; QString transcriptPath; QString transcriptOut; QString exportFmt; QString exportOut; bool statsRequested = false; bool loadLastRequested = false; bool smartSaveRequested = false; bool quickSaveRequested = false; bool exitRequested = false; bool versionRequested = false; static const struct { const char *flag; const char *fmt; } kFormats[] = { {"--zil", "zil"}, {"--inform6", "inform6"}, {"--inform7", "inform7"}, {"--alan", "alan"}, {"--quest", "quest"}, {"--questrooms", "questrooms"}, }; const QStringList args = QApplication::arguments(); for (int i = 1; i < args.size(); ++i) { if (args.at(i) == QLatin1String("--render") && i + 1 < args.size()) { renderPath = args.at(++i); continue; } if (args.at(i) == QLatin1String("--pdf") && i + 1 < args.size()) { pdfPath = args.at(++i); continue; } if (args.at(i) == QLatin1String("--save") && i + 1 < args.size()) { savePath = args.at(++i); continue; } if (args.at(i) == QLatin1String("--stats")) { statsRequested = true; continue; } if (args.at(i) == QLatin1String("--loadlastproject") || args.at(i) == QLatin1String("-a")) { loadLastRequested = true; continue; } if (args.at(i) == QLatin1String("--smartsave") || args.at(i) == QLatin1String("-s")) { smartSaveRequested = true; continue; } if (args.at(i) == QLatin1String("--quicksave") || args.at(i) == QLatin1String("-q")) { quickSaveRequested = true; continue; } if (args.at(i) == QLatin1String("--exit") || args.at(i) == QLatin1String("-x")) { exitRequested = true; continue; } if (args.at(i) == QLatin1String("--version") || args.at(i) == QLatin1String("-v")) { versionRequested = true; continue; } if (args.at(i) == QLatin1String("--import-transcript") && i + 2 < args.size()) { transcriptPath = args.at(++i); transcriptOut = args.at(++i); continue; } bool matched = false; for (const auto &f : kFormats) { if (args.at(i) == QLatin1String(f.flag) && i + 1 < args.size()) { exportFmt = QLatin1String(f.fmt); exportOut = args.at(++i); matched = true; break; } } if (!matched) mapPath = args.at(i); } // --version (C# had no equivalent): print the baked-in version and exit // before doing any batch work or opening the editor. Uses the same string // the About box shows via QApplication::applicationVersion(). if (versionRequested) { QTextStream(stdout) << QStringLiteral("trizbort-qt ") << QApplication::applicationVersion() << Qt::endl; return 0; } // --loadlastproject supplies the map when none was named on the command line. if (loadLastRequested && mapPath.isEmpty()) mapPath = lastProjectPath(); if (smartSaveRequested) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --smartsave" << Qt::endl; return 2; } return runSmartSave(mapPath); } if (quickSaveRequested) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --quicksave" << Qt::endl; return 2; } return runQuickSave(mapPath); } if (!exportFmt.isEmpty()) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt -- " << Qt::endl; return 2; } return runExport(exportFmt, mapPath, exportOut); } if (statsRequested) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --stats" << Qt::endl; return 2; } return runStats(mapPath); } if (!transcriptPath.isEmpty()) { return runImportTranscript(transcriptPath, transcriptOut); } if (!savePath.isEmpty()) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --save " << Qt::endl; return 2; } return runSave(mapPath, savePath); } if (!pdfPath.isEmpty()) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --pdf " << Qt::endl; return 2; } return renderToFile(mapPath, pdfPath, /*pdf=*/true); } if (!renderPath.isEmpty()) { if (mapPath.isEmpty()) { QTextStream(stderr) << "usage: trizbort-qt --render " << Qt::endl; return 2; } return renderToFile(mapPath, renderPath, /*pdf=*/false); } // --exit (C# -x): after any batch work, don't open the editor window. if (exitRequested) return 0; trizbort::MainWindow window; window.resize(1000, 700); if (!mapPath.isEmpty()) window.loadFile(mapPath); window.show(); return app.exec(); }