diff --git a/.editorconfig b/.editorconfig index bd78bcb..cbf86a3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,8 @@ insert_final_newline = true max_line_length = 80 tab_width = 4 +[*.ui] +tab_width = 2 + +[*.xml] +tab_width = 2 diff --git a/.gitignore b/.gitignore index 8d2f44b..9aa38c1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .flatpak *~ build - +buildgtk4 diff --git a/src/Config.vala.in b/Config.vala.in similarity index 100% rename from src/Config.vala.in rename to Config.vala.in diff --git a/FileFormat.txt b/FileFormat.txt new file mode 100644 index 0000000..fce3ae4 --- /dev/null +++ b/FileFormat.txt @@ -0,0 +1,63 @@ +[Description] +Random pattern // Title of game to show in headerbar +Gnonograms Generator // Author (default) +2024-12-11T10:36:56+0000 // Date game was saved +2 // Difficulty 0 = Easy, 1 = Moderate, 2 = Difficult, 3 = Challenging, 4 = Advanced, 5 = Ambiguous +// [License] (unused) +[Dimensions] +10 // Rows +10 // Columns +[Row clues] +1, 4 +4 +4 +1, 2 +1, 3, 1 +2 +5 +4, 3 +2, 3 +2, 2 +[Column clues] +1, 2, 2 +1, 2 +1, 1, 1 +1, 1, 3 +6 +1, 3 +2, 1, 1 +2, 1, 2 +2, 1, 2 +2, 1, 1 +[Solution grid] // 0 = Unknown, 1 = Empty, 2 = Filled (optional) +2 1 1 1 1 1 2 2 2 2 +1 1 1 1 1 1 2 2 2 2 +2 2 2 2 1 1 1 1 1 1 +2 1 1 1 1 1 1 2 2 1 +1 1 2 1 2 2 2 1 1 2 +1 1 1 2 2 1 1 1 1 1 +1 1 1 1 2 2 2 2 2 1 +1 1 2 2 2 2 1 2 2 2 +2 2 1 2 2 2 1 1 1 1 +2 2 1 2 2 1 1 1 1 1 +[Locked] // Whether game is read-only (modifications must be saved to different file +true +// Following only saved to temp file for restoring on startup +[Working grid] +2 0 0 0 0 0 0 0 0 0 +0 2 0 0 0 0 0 0 0 0 +0 0 2 0 0 0 0 0 0 0 +0 0 0 2 0 0 0 0 0 0 +0 0 0 0 2 0 0 0 0 0 +0 0 0 0 0 2 0 0 0 0 +0 0 0 0 0 2 2 0 0 0 +0 0 0 0 0 0 2 2 0 0 +0 0 0 0 0 0 0 2 2 0 +0 0 0 0 0 0 0 0 2 2 +[State] // 0 = Setting 1 = Solving +GNONOGRAMS_GAME_STATE_SOLVING +[Original path] // Path to original game +/home/jeremy/.config/unsaved/Unsaved Game.gno +[History] // Series of moves made when solving [row, col, current_state, previous_state;] +0,0,2,0;1,1,2,0;2,2,2,0;3,3,2,0;4,4,2,0;5,5,2,0;6,5,2,0;6,6,2,0;7,6,2,0;7,7,2,0;8,7,2,0;8,8,2,0;9,8,2,0;9,9,2,0; + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9cecc1d..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - 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. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - 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: - - {project} Copyright (C) {year} {fullname} - 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 -. diff --git a/com.github.jeremypw.gnonograms.yml b/com.github.jeremypw.gnonograms.yml index b9d677c..4174e72 100644 --- a/com.github.jeremypw.gnonograms.yml +++ b/com.github.jeremypw.gnonograms.yml @@ -1,9 +1,10 @@ app-id: com.github.jeremypw.gnonograms runtime: io.elementary.Platform -runtime-version: '7' +runtime-version: 'daily' sdk: io.elementary.Sdk command: com.github.jeremypw.gnonograms finish-args: + - '--device=dri' - '--share=ipc' - '--socket=wayland' - '--socket=fallback-x11' diff --git a/data/Application.css b/data/Application.css deleted file mode 100644 index 01f2607..0000000 --- a/data/Application.css +++ /dev/null @@ -1,30 +0,0 @@ - /* - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: - * Jeremy Wootten - */ - -@define-color GNONOGRAMS_DARK_PURPLE #180297; -@define-color GNONOGRAMS_PALE_PURPLE #cdc9e0; -.gnonograms-header { - border: solid; - border-top-width: 1px; - border-color: @GNONOGRAMS_DARK_PURPLE; - background-image: linear-gradient(to left, - alpha(@GNONOGRAMS_DARK_PURPLE, 1.0), alpha(@GNONOGRAMS_PALE_PURPLE, 0)); -} - diff --git a/data/com.github.jeremypw.gnonograms.appdata.xml.in b/data/com.github.jeremypw.gnonograms.appdata.xml.in index c4be02e..a0a9f0f 100644 --- a/data/com.github.jeremypw.gnonograms.appdata.xml.in +++ b/data/com.github.jeremypw.gnonograms.appdata.xml.in @@ -19,7 +19,32 @@ Game LogicGame + + + Solving a puzzle + https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsSolvingLight.png + + + Solving a puzzle (dark variant) + https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsSolvingDark.png + + + Designing a puzzle + https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsDesigningLight.png + + + Designing a puzzle (dark variant) + https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsDesigningDark.png + + + + +
    +
  • Port to Gtk-4
  • +
+
+
    @@ -202,24 +227,6 @@ com.github.jeremypw.gnonograms libgnonograms-core - - - Manually solving a puzzle - https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsSolvingLight.png - - - Manually solving a puzzle - https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsSolvingDark.png - - - Manually solving a puzzle - https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsDesigningLight.png - - - Manually solving a puzzle - https://raw.githubusercontent.com/jeremypw/gnonograms/master/data/screenshots/GnonogramsDesigningDark.png - - Jeremy Paul Wootten https://github.com/jeremypw/gnonograms https://github.com/jeremypw/gnonograms/issues diff --git a/data/gresource.xml b/data/gresource.xml deleted file mode 100644 index f62b853..0000000 --- a/data/gresource.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - icons/24/head-thinking-symbolic.svg - icons/24/head-thinking-symbolic.svg - Application.css - - diff --git a/data/icons/24/head-thinking-symbolic.svg b/data/icons/24/head-thinking-symbolic.svg index 7c3bbd0..e69de29 100644 --- a/data/icons/24/head-thinking-symbolic.svg +++ b/data/icons/24/head-thinking-symbolic.svg @@ -1,123 +0,0 @@ - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/data/schemas/com.github.jeremypw.gnonograms.gschema.xml b/data/schemas/com.github.jeremypw.gnonograms.gschema.xml index caf81cb..5baa362 100644 --- a/data/schemas/com.github.jeremypw.gnonograms.gschema.xml +++ b/data/schemas/com.github.jeremypw.gnonograms.gschema.xml @@ -1,21 +1,19 @@ - - - - - - - - + + + + + + - + @@ -43,13 +41,38 @@ - + + "rgba(24,18,151,1.0)" + Color of filled cell when solving + + The color used to mark a filled cell when solving a gnonogram puzzle. + Defaults to Gnonograms Purple. + + + + + "rgba(255,255,0,1.0)" + Color of empty cell when solving + + The color used to mark a empty cell when solving a gnonogram puzzle. + Defaults to yellow + + + + true - Visual hints in clues + Whether to follow system appearance style + + Whether to follow system 'prefers-dark' style setting or to use + the application preference. + + + + + false + Prefer dark style - Use strikethrough for each contiguous completed block from edge in clue label. - Mark clue in red if there is a definite error in the corresponding region. - Fade clue if corresponding region is completed without definite error. + Whether to use a dark style when not following the system style @@ -63,12 +86,15 @@ - - (0, 0) - Position of the window - - The x and y coordinate of the window origin. - + + 720 + Most recent window height + Most recent window height + + + 1024 + Most recent window width + Most recent window width '' @@ -77,12 +103,5 @@ The location where the current game is stored (if it is not an unsaved game). - - 32 - Size of individual cells - - The width and height, in pixels, of the square cells making up a puzzle grid). - - diff --git a/data/screenshots/GnonogramsDesigningDark.png b/data/screenshots/GnonogramsDesigningDark.png index bbe4c36..0a3eea1 100644 Binary files a/data/screenshots/GnonogramsDesigningDark.png and b/data/screenshots/GnonogramsDesigningDark.png differ diff --git a/data/screenshots/GnonogramsDesigningGame.gno b/data/screenshots/GnonogramsDesigningGame.gno new file mode 100644 index 0000000..f948834 --- /dev/null +++ b/data/screenshots/GnonogramsDesigningGame.gno @@ -0,0 +1,81 @@ +[Description] +Rhino +Unknown +2010/07/01 +13 +[Dimensions] +20 +30 +[Row clues] +3 +12 +11,3 +7,3 +2,3,2,2 +3,1,3,5 +3,1,3,2 +3,1,1,1,5 +3,3,1,1,1,4 +3,5,2,1,5 +1,2,7,1,5 +1,1,6,1,8 +1,1,6,10 +1,2,6,10,1 +2,2,3,2,4,5 +1,2,1,2,9 +1,2,1,3,7 +1,2,2,3,5 +2,2,1,3,3 +1,2,1,3 +[Column clues] +8 +5 +7,6 +3,5,2 +1 +2 +2,4,4 +2,2,8 +2,6,2 +3,5 +2,6 +2,6 +2,8 +2,5,3 +2,2 +2 +2,5 +2,2,8 +2,2,5,4 +1,3,1,4,2 +2,4,8 +2,12 +4,12 +3,7,4 +14 +3,7 +2,2,3 +1,2 +1 +1 +[Solution] +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 2 2 2 1 1 1 1 1 1 1 +1 1 1 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 +1 1 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 2 2 1 2 2 1 1 1 +1 2 2 2 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 2 2 2 2 2 1 1 1 +2 2 2 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 2 2 1 1 1 1 +2 2 2 1 1 1 2 1 1 1 1 1 1 2 1 1 1 2 1 1 2 2 2 2 2 1 1 1 1 1 +2 2 2 1 1 1 2 2 2 1 1 1 1 2 1 1 1 2 1 2 1 2 2 2 2 1 1 1 1 1 +2 2 2 1 1 1 1 2 2 2 2 2 1 2 2 1 1 1 2 1 2 2 2 2 2 1 1 1 1 1 +2 1 2 2 1 1 1 1 2 2 2 2 2 2 2 1 1 1 2 1 2 2 2 2 2 1 1 1 1 1 +2 1 1 2 1 1 1 1 2 2 2 2 2 2 1 1 1 1 2 1 2 2 2 2 2 2 2 2 1 1 +2 1 1 2 1 1 1 2 2 2 2 2 2 1 1 1 1 2 2 2 2 2 2 2 2 2 2 1 1 1 +2 1 2 2 1 1 1 2 2 2 2 2 2 1 1 1 2 2 2 2 2 2 2 2 2 2 1 1 1 2 +1 1 2 2 1 1 2 2 1 1 2 2 2 1 1 1 2 2 1 2 2 2 2 1 2 2 2 2 2 1 +1 1 2 1 1 1 2 2 1 1 1 1 2 1 1 1 2 2 1 2 2 2 2 2 2 2 2 2 1 1 +1 1 2 1 1 1 2 2 1 1 1 1 2 1 1 1 2 2 2 1 2 2 2 2 2 2 2 1 1 1 +1 1 2 1 1 1 2 2 1 1 1 1 2 2 1 1 2 2 2 1 1 2 2 2 2 2 1 1 1 1 +1 1 2 2 1 1 1 2 2 1 1 1 1 2 1 1 1 2 2 2 1 1 2 2 2 1 1 1 1 1 +1 1 1 2 1 1 1 2 2 1 1 1 1 2 1 1 1 2 2 2 1 1 1 1 1 1 1 1 1 1 diff --git a/data/screenshots/GnonogramsDesigningLight.png b/data/screenshots/GnonogramsDesigningLight.png index b6f2dec..0b6dc45 100644 Binary files a/data/screenshots/GnonogramsDesigningLight.png and b/data/screenshots/GnonogramsDesigningLight.png differ diff --git a/data/screenshots/GnonogramsSolvingDark.png b/data/screenshots/GnonogramsSolvingDark.png index 2d2441c..06395e6 100644 Binary files a/data/screenshots/GnonogramsSolvingDark.png and b/data/screenshots/GnonogramsSolvingDark.png differ diff --git a/data/screenshots/GnonogramsSolvingLight.png b/data/screenshots/GnonogramsSolvingLight.png index 5ffaed0..ce2b1c2 100644 Binary files a/data/screenshots/GnonogramsSolvingLight.png and b/data/screenshots/GnonogramsSolvingLight.png differ diff --git a/libcore/Enums.vala b/libcore/Enums.vala deleted file mode 100644 index 061b2f2..0000000 --- a/libcore/Enums.vala +++ /dev/null @@ -1,105 +0,0 @@ -/* Enums.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -namespace Gnonograms { - public enum Difficulty { - TRIVIAL = 0, - VERY_EASY = 1, - EASY = 2, - MODERATE = 3, - HARD = 4 , - CHALLENGING = 5, - ADVANCED = 6, - MAXIMUM = 7, /* Max grade for generated puzzles (possibly ambiguous)*/ - COMPUTER = 8, /* Grade for requested computer solving */ - UNDEFINED = 99; - - public string to_string () { - switch (this) { - case Difficulty.TRIVIAL: - return _("Trivial"); - case Difficulty.VERY_EASY: - return _("Very Easy"); - case Difficulty.EASY: - return _("Easy"); - case Difficulty.MODERATE: - return _("Moderately difficult"); - case Difficulty.HARD: - return _("Difficult"); - case Difficulty.CHALLENGING: - return _("Very Difficult"); - case Difficulty.ADVANCED: - return _("Advanced logic required"); - case Difficulty.MAXIMUM: - return _("Possibly ambiguous"); - case Difficulty.COMPUTER: - return _("Super human"); - case Difficulty.UNDEFINED: - return ""; - default: - critical ("grade to string - unexpected grade"); - assert_not_reached (); - } - } - - public static Difficulty[] all_human () { - return { EASY, MODERATE, HARD, CHALLENGING, ADVANCED, MAXIMUM }; - } - } - - public enum GameState { - SETTING, - SOLVING, - GENERATING, - UNDEFINED = 99; - } - - public enum CellState { - UNKNOWN, - EMPTY, - FILLED, - COMPLETED, - UNDEFINED; - } - - public enum SolverState { - ERROR = 0, - CANCELLED = 1, - NO_SOLUTION = 1 << 1, - SIMPLE = 1 << 2, - ADVANCED = 1 << 3, - AMBIGUOUS = 1 << 4, - UNDEFINED = 1 << 5; - - public bool solved () { - return this == SIMPLE || this == ADVANCED || this == AMBIGUOUS; - } - } - - public enum CellPatternType { - CELL, - HIGHLIGHT, - UNDEFINED - } - - public enum GamePatternType { - SIMPLE_RANDOM, - UNDEFINED - } -} diff --git a/libcore/Filewriter.vala b/libcore/Filewriter.vala deleted file mode 100644 index 253e6d2..0000000 --- a/libcore/Filewriter.vala +++ /dev/null @@ -1,192 +0,0 @@ -/* Filewriter.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Author: Jeremy Wootten < jeremwootten@gmail.com > - */ - -public class Gnonograms.Filewriter : Object { - public DateTime date { get; construct; } - public History? history { get; construct; } - public Gtk.Window? parent { get; construct; } - public Difficulty difficulty { get; set; default = Difficulty.UNDEFINED;} - public GameState game_state { get; set; default = GameState.UNDEFINED;} - public My2DCellArray? solution { get; set; default = null;} - public My2DCellArray? working { get; set; default = null;} - public uint rows { get; construct; } - public uint cols { get; construct; } - public string name { get; set; } - public string[] row_clues { get; construct; } - public string[] col_clues { get; construct; } - public bool save_solution { get; construct; } - public string? game_path { get; private set; } - public string author { get; set; default = "";} - public string license { get; set; default = "";} - public bool is_readonly { get; set; default = true;} - - - private FileStream? stream; - - public Filewriter (Gtk.Window? parent, - Dimensions dimensions, - string[] row_clues, - string[] col_clues, - History? history, - bool save_solution) throws IOError { - - Object ( - name: _(UNTITLED_NAME), - parent: parent, - rows: dimensions.height, - cols: dimensions.width, - row_clues: row_clues, - col_clues: col_clues, - history: history, - save_solution: save_solution - ); - } - - construct { - date = new DateTime.now_local (); - } - - /*** Writes minimum information required for valid game file ***/ - public void write_game_file (string? save_dir_path = null, - string? path = null, - string? _name = null) throws IOError { - - if (_name != null) { - name = _name; - } else { - name = _(UNTITLED_NAME); - } - - if (path == null || path.length <= 4) { - game_path = Utils.get_open_save_path (parent, - _("Name and save this puzzle"), - true, - save_dir_path, - name - ); - } else { - game_path = path; - } - - if (game_path != null && - (game_path.length < 4 || - game_path[-4 : game_path.length] != Gnonograms.GAMEFILEEXTENSION)) { - - game_path = game_path + Gnonograms.GAMEFILEEXTENSION; - } - - if (game_path == null) { - throw new IOError.CANCELLED ("No path selected"); - } - - var file = File.new_for_commandline_arg (game_path); - if (file.query_exists () && - !Utils.show_confirm_dialog (_("Overwrite %s").printf (game_path), - _("This action will destroy contents of that file"))) { - - throw new IOError.CANCELLED ("File exists"); - } - - stream = FileStream.open (game_path, "w"); /* This requires local path, not a uri */ - if (stream == null) { - throw new IOError.FAILED ("Could not open filestream to %s".printf (game_path)); - } - - if (name == null || name.length == 0) { - throw new IOError.NOT_INITIALIZED ("No name to save"); - } - - stream.printf ("[Description]\n"); - stream.printf ("%s\n", name); - stream.printf ("%s\n", author); - stream.printf ("%s\n", date.to_string ()); - stream.printf ("%u\n", difficulty); - - if (license == null || license.length > 0) { - stream.printf ("[License]\n"); - stream.printf ("%s\n", license); - } - - if (rows == 0 || cols == 0) { - throw new IOError.NOT_INITIALIZED ("No dimensions to save"); - } - - stream.printf ("[Dimensions]\n"); - stream.printf ("%u\n", rows); - stream.printf ("%u\n", cols); - - if (row_clues.length == 0 || col_clues.length == 0) { - throw new IOError.NOT_INITIALIZED ("No clues to save"); - } - - if (row_clues.length != rows || col_clues.length != cols) { - throw new IOError.NOT_INITIALIZED ("Clues do not match dimensions"); - } - - stream.printf ("[Row clues]\n"); - foreach (string s in row_clues) { - stream.printf ("%s\n", s); - } - - stream.printf ("[Column clues]\n"); - foreach (string s in col_clues) { - stream.printf ("%s\n", s); - } - - stream.flush (); - - if (solution != null && save_solution) { - stream.printf ("[Solution grid]\n"); - stream.printf ("%s", solution.to_string ()); - } - - stream.printf ("[Locked]\n"); - stream.printf (is_readonly.to_string () + "\n"); - } - - /*** Writes complete information to reload game state ***/ - public void write_position_file (string? save_dir_path = null, - string? path = null, - string? name = null) throws IOError { - if (working == null) { - throw (new IOError.NOT_INITIALIZED ("No working grid to save")); - } else if (game_state == GameState.UNDEFINED) { - throw (new IOError.NOT_INITIALIZED ("No game state to save")); - } - - write_game_file (save_dir_path, path, name ); - stream.printf ("[Working grid]\n"); - stream.printf (working.to_string ()); - stream.printf ("[State]\n"); - stream.printf (game_state.to_string () + "\n"); - - if (name != _(UNTITLED_NAME)) { - stream.printf ("[Original path]\n"); - stream.printf (game_path.to_string () + "\n"); - } - - if (history != null) { - stream.printf ("[History]\n"); - stream.printf (history.to_string () + "\n"); - } - - stream.flush (); - } -} diff --git a/libcore/History.vala b/libcore/History.vala deleted file mode 100644 index a243a9c..0000000 --- a/libcore/History.vala +++ /dev/null @@ -1,168 +0,0 @@ -/* History.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -public class Gnonograms.History : GLib.Object { - public bool can_go_back { get; private set; } - public bool can_go_forward { get; private set; } - - private HistoryStack back_stack; - private HistoryStack forward_stack; - - construct { - back_stack = new HistoryStack (); - forward_stack = new HistoryStack (); - - back_stack.notify["empty"].connect (() => { - can_go_back = !back_stack.empty; - }); - - forward_stack.notify["empty"].connect (() => { - can_go_forward = !forward_stack.empty; - }); - } - - public void clear_all () { - forward_stack.clear (); - back_stack.clear (); - } - - public void record_move (Cell cell, CellState previous_state) { - var new_move = new Gnonograms.Move (cell, previous_state); - if (new_move.cell.state != CellState.UNDEFINED) { - Move last_move = back_stack.peek_move (); - if (last_move.equal (new_move)) { - return; - } - - forward_stack.clear (); - } - - back_stack.push_move (new_move); - } - - public Move pop_next_move () { - Move mv = forward_stack.pop_move (); - back_stack.push_move (mv); - return mv; - } - - public Move pop_previous_move () { - Move mv = back_stack.pop_move (); - /* Record copy otherwise it will be altered by next line*/ - forward_stack.push_move (mv.clone ()); - mv.cell.state = mv.previous_state; - return mv; - } - - public Move? get_current_move () { - return back_stack.peek_move (); - } - - public string to_string () { - return back_stack.to_string () + forward_stack.to_string (); - } - - public void from_string (string? s) { - clear_all (); - if (s == null) { - return; - } - - var stacks = Utils.remove_blank_lines (s.split ("\n")); - if (stacks != null) { - add_to_stack_from_string (stacks[0], true); - } - - if (stacks.length > 1) { - add_to_stack_from_string (stacks[1], false); - } - } - - private void add_to_stack_from_string (string? s, bool back) { - if (s == null) { - return; - } - - var moves_s = s.split (";"); - if (moves_s == null) { - return; - } - - foreach (string move_s in moves_s) { - var move = Move.from_string (move_s); - if (move != null) { - if (back) { - back_stack.push_move (move); - } else { - forward_stack.push_move (move); - } - } - } - } - - private class HistoryStack : Object { - public bool empty { get; private set; } - - private Gee.Deque stack; - - construct { - stack = new Gee.LinkedList (); - } - - public void push_move (Move mv) { - if (!mv.is_null ()) { - stack.offer_head (mv); - empty = false; - } - } - - public Move peek_move () { - if (empty) { - return Move.null_move; - } else { - return stack.peek_head (); - } - } - - public Move pop_move () { - Move mv = Move.null_move; - if (!empty) { - mv = stack.poll_head (); - } - - empty = stack.is_empty; - return mv; - } - - public void clear () { - stack.clear (); - empty = true; - } - - public string to_string () { - var sb = new StringBuilder (""); - foreach (Move mv in stack) { /* iterates from head backwards */ - sb.prepend (mv.to_string () + ";"); - } - - sb.append ("\n"); - return sb.str; - } - } -} diff --git a/libcore/Move.vala b/libcore/Move.vala deleted file mode 100644 index ff35e8b..0000000 --- a/libcore/Move.vala +++ /dev/null @@ -1,78 +0,0 @@ -/* Move.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -public class Gnonograms.Move { - public static Move null_move = new Move (NULL_CELL, CellState.UNDEFINED); - - public Cell cell; - public CellState previous_state; - - public Move (Cell _cell, CellState _previous_state) { - cell = Cell () { - row =_cell.row, - col =_cell.col, - state = _cell.state - }; - - previous_state = _previous_state; - } - - public bool equal (Move m) { - return m.cell.equal (cell) && m.previous_state == previous_state; - } - - public Move clone () { - return new Move (this.cell.clone (), this.previous_state); - } - - public bool is_null () { - return equal (Move.null_move); - } - - public string to_string () { - return "%u,%u,%u,%u".printf (cell.row, cell.col, cell.state, previous_state); - } - - public static Move from_string (string? s) { - if (s == null) { - return Move.null_move; - } - - var parts = s.split (","); - if (parts == null || parts.length != 4) { - return Move.null_move; - } - - var row = (uint)(int.parse (parts[0])); - var col = (uint)(int.parse (parts[1])); - var state = (Gnonograms.CellState)(int.parse (parts[2])); - var previous_state = (Gnonograms.CellState)(int.parse (parts[3])); - - if (row > Gnonograms.MAXSIZE || - col > Gnonograms.MAXSIZE || - state == Gnonograms.CellState.UNDEFINED || - previous_state == Gnonograms.CellState.UNDEFINED) { - - return Move.null_move; - } - - Cell c = {row, col, state}; - return new Move (c, previous_state); - } -} diff --git a/libcore/Structs.vala b/libcore/Structs.vala deleted file mode 100644 index 0e7a74d..0000000 --- a/libcore/Structs.vala +++ /dev/null @@ -1,112 +0,0 @@ -/* Structs.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -public class Gnonograms.Block { - public int length; - public bool is_complete; - public bool is_error; - - public Block (int len, bool complete = false, bool error = false) { - length = len; - is_complete= complete; - is_error = error; - } - - public Block.null () { - length = -1; - is_complete = false; - is_error = false; - } - - public bool is_null () { - return length < 0; - } -} - -public struct Gnonograms.Cell { - public uint row; - public uint col; - public CellState state; - - public bool same_coords (Cell c) { - return (this.row == c.row && this.col == c.col); - } - - public bool equal (Cell b) { - return ( - this.row == b.row && - this.col == b.col && - this.state == b.state - ); - - } - - public Cell inverse () { - Cell c = {row, col, CellState.UNKNOWN }; - - if (this.state == CellState.EMPTY) { - c.state = CellState.FILLED; - } else { - c.state = CellState.EMPTY; - } - - return c; - } - - public Cell clone () { - return { row, col, state }; - } - - public string to_string () { - return "Row %u, Col %u, State %s".printf (row, col, state.to_string ()); - } -} - -public struct Gnonograms.Dimensions { - uint width; - uint height; - - public uint area () { - return width * height; - } - - public uint length () { - return width + height; - } - - public bool equal (Dimensions other) { - return width == other.width && height == other.height; - } -} - -public struct Gnonograms.FilterInfo { - string name; - string[] patterns; -} - -public struct Gnonograms.Range { //can use for filled subregions or ranges of filled and unknown cells - public int start; - public int end; - public int filled; - public int unknown; - - public int length () { - return end - start + 1; - } -} diff --git a/libcore/widgets/Cellgrid.vala b/libcore/widgets/Cellgrid.vala deleted file mode 100644 index 503db44..0000000 --- a/libcore/widgets/Cellgrid.vala +++ /dev/null @@ -1,367 +0,0 @@ -/* CellGrid.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -public class Gnonograms.CellGrid : Gtk.DrawingArea { - public signal void cursor_moved (Cell from, Cell to); - - public View view { get; construct; } - public Cell current_cell { get; set; } - public Cell previous_cell { get; set; } - public bool frozen { get; set; } - public bool draw_only { get; set; default = false;} - - /* Could have more options for cell pattern*/ - private CellPatternType _cell_pattern_type; - public CellPatternType cell_pattern_type { - get { - return _cell_pattern_type; - } - - set { - switch (value) { - case CellPatternType.CELL: /* plain color fill */ - filled_cell_pattern = new CellPattern.cell (fill_color); - empty_cell_pattern = new CellPattern.cell (empty_color); - unknown_cell_pattern = new CellPattern.cell (unknown_color); - _cell_pattern_type = value; - - break; - default: - /* Refresh colors of existing pattern */ - if (_cell_pattern_type != CellPatternType.UNDEFINED) { - cell_pattern_type = _cell_pattern_type; - } - - break; - } - } - } - - private const double MAJOR_GRID_LINE_WIDTH = 3.0; - private const double MINOR_GRID_LINE_WIDTH = 1.0; - private Gdk.RGBA[, ] colors; - - private int rows = 0; - private int cols = 0; - private bool dirty = false; /* Whether a redraw is needed */ - private double cell_width; /* Width of cell including frame */ - private double cell_height; /* Height of cell including frame */ - - private Gdk.RGBA grid_color; - private Gdk.RGBA fill_color; - private Gdk.RGBA empty_color; - private Gdk.RGBA unknown_color; - - private CellPattern filled_cell_pattern; - private CellPattern empty_cell_pattern; - private CellPattern unknown_cell_pattern; - private CellPattern highlight_pattern; - - private My2DCellArray? array { - get { - return view.model.display_data; - } - } - - public CellGrid (View view) { - Object ( - view: view - ); - } - - construct { - _current_cell = NULL_CELL; - colors = new Gdk.RGBA[2, 3]; - grid_color.parse (Gnonograms.GRID_COLOR); - cell_pattern_type = CellPatternType.CELL; - set_colors (); - - - this.add_events ( - Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK | - Gdk.EventMask.POINTER_MOTION_MASK | - Gdk.EventMask.KEY_PRESS_MASK | - Gdk.EventMask.KEY_RELEASE_MASK | - Gdk.EventMask.LEAVE_NOTIFY_MASK - ); - - motion_notify_event.connect (on_pointer_moved); - draw.connect (on_draw_event); - leave_notify_event.connect (on_leave_notify); - - notify["current-cell"].connect (() => { - queue_draw (); - }); - - view.notify["cell-size"].connect (dimensions_updated); - view.controller.notify["dimensions"].connect (dimensions_updated); - view.controller.notify["game-state"].connect (() => { - var gs = view.controller.game_state; - unknown_color = colors[(int)gs, (int)CellState.UNKNOWN]; - fill_color = colors[(int)gs, (int)CellState.FILLED]; - empty_color = colors[(int)gs, (int)CellState.EMPTY]; - cell_pattern_type = CellPatternType.UNDEFINED; /* Causes refresh of existing pattern */ - }); - - view.model.changed.connect (() => { - if (!dirty) { - dirty = true; - queue_draw (); - } - }); - - dimensions_updated (); - } - - private void set_colors () { - int setting = (int)GameState.SETTING; - colors[setting, (int)CellState.UNKNOWN].parse (Gnonograms.UNKNOWN_COLOR); - colors[setting, (int)CellState.EMPTY].parse (Gnonograms.SETTING_EMPTY_COLOR); - colors[setting, (int)CellState.FILLED].parse (Gnonograms.SETTING_FILLED_COLOR); - - int solving = (int)GameState.SOLVING; - colors[solving, (int)CellState.UNKNOWN].parse (Gnonograms.UNKNOWN_COLOR); - colors[solving, (int)CellState.EMPTY].parse (Gnonograms.SOLVING_EMPTY_COLOR); - colors[solving, (int)CellState.FILLED].parse (Gnonograms.SOLVING_FILLED_COLOR); - } - - private void dimensions_updated () { - rows = (int)view.controller.dimensions.height; - cols = (int)view.controller.dimensions.width; - cell_width = view.cell_size; - cell_height = view.cell_size; - /* Cause refresh of existing pattern */ - highlight_pattern = new CellPattern.highlight (cell_width, cell_height); - set_size_request (cols * view.cell_size + (int)MINOR_GRID_LINE_WIDTH, rows * view.cell_size + (int)MINOR_GRID_LINE_WIDTH); - } - - private bool on_draw_event (Cairo.Context cr) { - dirty = false; - - if (array != null) { - /* Note, even tho' array holds CellStates, its iterator returns Cells */ - foreach (Cell c in array) { - bool highlight = (c.row == current_cell.row && c.col == current_cell.col); - draw_cell (cr, c, highlight); - } - } - - draw_grid (cr); - return true; - } - - private bool on_pointer_moved (Gdk.EventMotion e) { - if (draw_only || e.x < 0 || e.y < 0) { - return false; - } - /* Calculate which cell the pointer is over */ - uint r = ((uint)((e.y) / cell_height)); - uint c = ((uint)(e.x / cell_width)); - if (r >= rows || c >= cols) { - return true; - } - /* Construct cell beneath pointer */ - Cell cell = {r, c, array.get_data_from_rc (r, c)}; - if (!cell.equal (current_cell)) { - update_current_cell (cell); - } - - return true; - } - - private void update_current_cell (Cell target) { - previous_cell = current_cell.clone (); - current_cell = target.clone (); - } - - private void draw_grid (Cairo.Context cr) { - Gdk.cairo_set_source_rgba (cr, grid_color); - cr.set_antialias (Cairo.Antialias.NONE); - cr.set_line_width (MINOR_GRID_LINE_WIDTH); - - // Draw minor grid lines - double y1 = MINOR_GRID_LINE_WIDTH; - double x1 = MINOR_GRID_LINE_WIDTH; - double x2 = x1 + cols * view.cell_size; - double y2 = y1 + rows * view.cell_size; - while (y1 < y2) { - cr.move_to (x1, y1); - cr.line_to (x2, y1); - cr.stroke (); - y1 += view.cell_size; - } - - y1 = MINOR_GRID_LINE_WIDTH; - // x1 = MINOR_GRID_LINE_WIDTH; - while (x1 < x2) { - cr.move_to (x1, y1); - cr.line_to (x1, y2); - cr.stroke (); - x1 += view.cell_size; - } - - // Draw inner major grid lines - cr.set_line_width (MAJOR_GRID_LINE_WIDTH); - x1 = MINOR_GRID_LINE_WIDTH; - while (y1 < y2) { - y1 += 5.0 * view.cell_size; - cr.move_to (x1, y1); - cr.line_to (x2, y1); - cr.stroke (); - } - - y1 = MINOR_GRID_LINE_WIDTH; - while (x1 < x2) { - x1 += 5.0 * view.cell_size; - cr.move_to (x1, y1); - cr.line_to (x1, y2); - cr.stroke (); - } - - // Draw frame - cr.set_line_width (MINOR_GRID_LINE_WIDTH); - y1 = 0; - x1 = 0; - cr.move_to (x1, y1); - cr.line_to (x2, y1); - cr.stroke (); - - cr.line_to (x2, y2); - cr.stroke (); - - cr.line_to (x1, y2); - cr.stroke (); - - cr.line_to (x1, y1); - cr.stroke (); - } - - private void draw_cell (Cairo.Context cr, Cell cell, bool highlight = false, bool mark = false) { - if (frozen) { - return; - } - - double x = cell.col * cell_width; - double y = cell.row * cell_height; - CellPattern cell_pattern; - switch (cell.state) { - case CellState.EMPTY: - cell_pattern = empty_cell_pattern; - break; - - case CellState.FILLED: - cell_pattern = filled_cell_pattern; - break; - - default : - cell_pattern = unknown_cell_pattern; - break; - } - - cr.save (); - cell_pattern.move_to (x, y); /* Not needed for plain fill, but may use a pattern later */ - cr.set_line_width (0.0); - cr.rectangle (x, y, cell_width, cell_height); - cr.set_source (cell_pattern.pattern); - cr.fill (); - cr.restore (); - - if (highlight && !draw_only) { - cr.save (); - /* Ensure highlight centred and slightly overlapping grid */ - highlight_pattern.move_to (x, y); - cr.rectangle (x, y, cell_width, cell_width); - cr.clip (); - cr.set_source (highlight_pattern.pattern); - cr.set_operator (Cairo.Operator.OVER); - cr.paint (); - cr.restore (); - } - } - - private bool on_leave_notify () { - previous_cell = NULL_CELL; - current_cell = NULL_CELL; - return false; - } - - private class CellPattern { - public Cairo.Pattern pattern; - public double size { get; private set; } - private double red; - private double green; - private double blue; - private double x0 = 0; - private double y0 = 0; - private Cairo.Matrix matrix; - - public CellPattern.cell (Gdk.RGBA color) { - red = color.red; - green = color.green; - blue = color.blue; - matrix = Cairo.Matrix.identity (); - - var granite_settings = Granite.Settings.get_default (); - set_pattern (granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK); - - granite_settings.notify["prefers-color-scheme"].connect (() => { - set_pattern (granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK); - }); - } - - public CellPattern.highlight (double wd, double ht) { - var r = (wd + ht) / 4.0; - size = 2 * r; - - Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, (int)size, (int)size); - Cairo.Context context = new Cairo.Context (surface); - context.set_source_rgb (0.0, 0.0, 0.0); - context.rectangle (0, 0, size, size); - context.fill (); - context.arc (r, r, r - 2.0, 0, 2 * Math.PI); - context.set_source_rgba (1.0, 1.0, 1.0, 0.5); - context.set_operator (Cairo.Operator.SOURCE); - context.fill (); - - pattern = new Cairo.Pattern.for_surface (surface); - pattern.set_extend (Cairo.Extend.NONE); - matrix = Cairo.Matrix.identity (); - pattern.set_matrix (matrix); - } - - public void move_to (double x, double y) { - var xx = x - x0; - var yy = y - y0; - matrix.translate (-xx, -yy); - pattern.set_matrix (matrix); - x0 = x; - y0 = y; - } - - private void set_pattern (bool is_dark) { - pattern = new Cairo.Pattern.rgba ( - is_dark ? red / 2 : red, - is_dark ? green / 2 : green, - is_dark ? blue / 2 : blue, - 1.0 - ); - } - } -} diff --git a/libcore/widgets/Labelbox.vala b/libcore/widgets/Labelbox.vala deleted file mode 100644 index 0d3c8df..0000000 --- a/libcore/widgets/Labelbox.vala +++ /dev/null @@ -1,150 +0,0 @@ -/* Labelbox.vala - * Copyright (C) 2010 - 2021 Jeremy Wootten - * - 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 . - * - * Author: Jeremy Wootten - */ - -public class Gnonograms.LabelBox : Gtk.Grid { - public View view { get; construct; } - - private uint n_labels = 0; - private uint n_cells = 0; - - public LabelBox (Gtk.Orientation _orientation, View view) { - Object (view: view, - column_homogeneous: true, - row_homogeneous: true, - column_spacing: 0, - row_spacing: 0, - orientation: _orientation, - expand: false - ); - } - - construct { - view.notify["cell-size"].connect (() => { - get_children ().foreach ((w) => { - ((Gnonograms.Clue)w).cell_size = view.cell_size; - }); - - set_size (); - }); - - view.controller.notify ["dimensions"].connect (() => { - var new_n_labels = orientation == Gtk.Orientation.HORIZONTAL ? - view.controller.dimensions.width : - view.controller.dimensions.height; - - var new_n_cells = orientation == Gtk.Orientation.HORIZONTAL ? - view.controller.dimensions.height : - view.controller.dimensions.width; - - if (new_n_labels != n_labels || new_n_cells != n_cells) { - n_labels = new_n_labels; - n_cells = new_n_cells; - change_n_labels (); - } - }); - - show_all (); - } - - private Gnonograms.Clue? get_label (uint index) { - var n_children = get_children ().length (); - if (index >= n_children) { - return null; - } else { - return (Gnonograms.Clue)(get_children ().nth_data (n_children - index - 1)); - } - } - - public string[] get_clues () { - string[] clues = new string [n_labels]; - var index = n_labels; - foreach (var widget in get_children ()) { // Delivers widgets in reverse order they were added - index--; - clues[index] = ((Clue)widget).clue; - } - - return clues; - } - - public void highlight (uint index, bool is_highlight) { - var label = get_label (index); - if (label != null) { - label.highlight (is_highlight); - } - } - - public void unhighlight_all () { - get_children ().foreach ((w) => { - ((Gnonograms.Clue)w).highlight (false); - }); - } - - public void update_label_text (uint index, string? txt) { - var label = get_label (index); - if (label != null) { - label.clue = txt ?? _(BLANKLABELTEXT); - } - } - - public void clear_formatting (uint index) { - var label = get_label (index); - if (label != null) { - label.clear_formatting (); - } - } - - public void update_label_complete (uint index, Gee.List grid_blocks) { - var label = get_label (index); - if (label != null) { - label.update_complete (grid_blocks); - } - } - - private void change_n_labels () { - foreach (var child in get_children ()) { - child.destroy (); - } - - for (var i = 0; i < n_labels; i++) { - var label = new Clue (orientation == Gtk.Orientation.HORIZONTAL) { - n_cells = this.n_cells, - cell_size = view.cell_size - }; - - add (label); - } - - set_size (); - show_all (); - } - - private void set_size () { - int width = (int)(orientation == Gtk.Orientation.HORIZONTAL ? - n_labels * view.cell_size : - n_cells * view.cell_size * GRID_LABELBOX_RATIO - ); - - int height = (int)(orientation == Gtk.Orientation.HORIZONTAL ? - n_cells * view.cell_size * GRID_LABELBOX_RATIO : - n_labels * view.cell_size - ); - - set_size_request (width, height); - } -} diff --git a/meson.build b/meson.build index 094eebf..ebf9876 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project ( 'com.github.jeremypw.gnonograms', 'vala', 'c', - version: '2.1.2', + version: '4.0.0', meson_version: '>= 0.58.0' ) @@ -13,23 +13,16 @@ if get_option('with_debugging') add_project_arguments('--define=WITH_DEBUGGING', language: 'vala') endif - i18n = import ('i18n') gnome = import('gnome') -gresource = gnome.compile_resources( - 'gresource', - 'data/gresource.xml', - source_dir: 'data' -) - config_data = configuration_data() config_data.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) config_data.set_quoted('GETTEXT_PACKAGE', meson.project_name()) config_data.set_quoted('VERSION', meson.project_version()) config_data.set_quoted('APP_ID', meson.project_name()) config_file = configure_file( - input: 'src/Config.vala.in', + input: 'Config.vala.in', output: '@BASENAME@', configuration: config_data ) @@ -37,35 +30,52 @@ config_file = configure_file( gnonogram_deps = [ dependency('glib-2.0'), dependency('gobject-2.0'), - dependency('granite', version: '>=6.2.0'), - dependency('gtk+-3.0'), + dependency('gtk4', version: '>=4.10'), dependency('gee-0.8', version: '>=0.8.5'), - dependency('libhandy-1', version: '>=1.2.0') + dependency('granite-7'), + dependency('libadwaita-1') ] executable ( meson.project_name (), - gresource, 'src/Application.vala', 'src/Controller.vala', 'src/View.vala', + 'src/Model.vala', + + 'src/HeaderBar/HeaderBarManager.vala', + 'src/HeaderBar/HeaderButton.vala', + 'src/HeaderBar/PopoverButton.vala', + 'src/HeaderBar/ProgressIndicator.vala', + 'src/HeaderBar/RestartButton.vala', + 'src/HeaderBar/AppPopover.vala', + 'src/HeaderBar/PreferenceRow.vala', + + 'src/dialogs/PreferencesDialog.vala', + + 'src/ui/shortcuthelper.ui.vala', + + 'src/widgets/Cluebox.vala', + 'src/widgets/Clue.vala', + 'src/widgets/Cellgrid.vala', + 'src/widgets/CellPattern.vala', + + 'src/objects/My2DCellArray.vala', + 'src/objects/Region.vala', + 'src/objects/Move.vala', + 'src/services/RandomPatternGenerator.vala', 'src/services/RandomGameGenerator.vala', - 'libcore/widgets/Labelbox.vala', - 'libcore/widgets/Label.vala', - 'libcore/widgets/Cellgrid.vala', - 'libcore/utils.vala', - 'libcore/Model.vala', - 'libcore/My2DCellArray.vala', - 'libcore/Region.vala', - 'libcore/Solver.vala', - 'libcore/Filereader.vala', - 'libcore/Filewriter.vala', - 'libcore/Move.vala', - 'libcore/History.vala', - 'libcore/Enums.vala', - 'libcore/Structs.vala', - 'libcore/Constants.vala', + 'src/services/ShortcutHelper.vala', + 'src/services/Solver.vala', + 'src/services/Filereader.vala', + 'src/services/Filewriter.vala', + 'src/services/History.vala', + + 'src/misc/utils.vala', + 'src/misc/Enums.vala', + 'src/misc/Structs.vala', + 'src/misc/Constants.vala', config_file, dependencies : gnonogram_deps, install: true diff --git a/meson_options.txt b/meson_options.txt index ecc3110..0a1258d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1 +1 @@ -option('with_debugging', type : 'boolean', value : 'false', description : 'Include code used for debugging the solver') +option('with_debugging', type : 'boolean', value : false, description : 'Include code used for debugging the solver') diff --git a/src/Application.vala b/src/Application.vala index 8dbd13f..6c7bc38 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -1,23 +1,52 @@ -/* Application.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ +namespace Gnonograms { + public enum GameState { + SETTING, + SOLVING, + GENERATING, + LOAD_SAVE; + } -public class Gnonograms.App : Gtk.Application { + public const string ACTION_GROUP = "win"; + public const string ACTION_PREFIX = ACTION_GROUP + "."; + public const string ACTION_UNDO = "action-undo"; + public const string ACTION_REDO = "action-redo"; + public const string ACTION_CURSOR_UP = "action-cursor_up"; + public const string ACTION_CURSOR_DOWN = "action-cursor_down"; + public const string ACTION_CURSOR_LEFT = "action-cursor_left"; + public const string ACTION_CURSOR_RIGHT = "action-cursor_right"; + public const string ACTION_SETTING_MODE = "action-setting-mode"; + public const string ACTION_SOLVING_MODE = "action-solving-mode"; + public const string ACTION_GENERATING_MODE = "action-generating-mode"; + public const string ACTION_OPEN = "action-open"; + public const string ACTION_SAVE = "action-save"; + public const string ACTION_SAVE_AS = "action-save-as"; + public const string ACTION_CHECK_ERRORS = "action-check-errors"; + public const string ACTION_RESTART = "action-restart"; + public const string ACTION_COMPUTER_SOLVE = "action-solve"; + public const string ACTION_HINT = "action-hint"; + public const string ACTION_OPTIONS = "action-options"; + public const string ACTION_OPTIONS_ACCEL = ""; + public const string ACTION_ZOOM_SMALLER = "action-zoom-smaller"; + public const string ACTION_ZOOM_DEFAULT = "action-zoom-default"; + public const string ACTION_ZOOM_LARGER = "action-zoom-larger"; + public const string ACTION_SHORTCUT_WINDOW = "action-shortcut-window"; + public const string ACTION_ABOUT_WINDOW = "action-about-dialog"; + public const string ACTION_PREFERENCES = "action-preferences"; + +#if WITH_DEBUGGING + public const string ACTION_DEBUG_ROW = "action-debug-row"; + public const string ACTION_DEBUG_COL = "action-debug-col"; +#endif + public GLib.Settings saved_state; + public GLib.Settings settings; + + public class App : Gtk.Application { private Controller controller; public App () { @@ -33,24 +62,19 @@ public class Gnonograms.App : Gtk.Application { GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); + saved_state = new GLib.Settings (Config.APP_ID + ".saved-state"); + settings = new GLib.Settings (Config.APP_ID + ".settings"); + SimpleAction quit_action = new SimpleAction ("quit", null); quit_action.activate.connect (() => { + warning ("quit action"); if (controller != null) { - controller.quit (); /* Will save state */ + controller.on_delete_request (); /* Will save state */ } }); add_action (quit_action); set_accels_for_action ("app.quit", {"q"}); - - var granite_settings = Granite.Settings.get_default (); - var gtk_settings = Gtk.Settings.get_default (); - - gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - - granite_settings.notify["prefers-color-scheme"].connect (() => { - gtk_settings.gtk_application_prefer_dark_theme = granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; - }); } public override void open (File[] files, string hint) { @@ -64,8 +88,7 @@ public class Gnonograms.App : Gtk.Application { public override void activate () { if (controller == null) { - controller = new Controller (); - controller.quit_app.connect (quit); + controller = Controller.get_default (); add_window (controller.window); } else { controller.window.present (); @@ -85,7 +108,6 @@ public static int main (string[] args) { var opt_context = new OptionContext (N_("[Gnonogram Puzzle File (.gno)]")); opt_context.set_translation_domain (Config.APP_ID); opt_context.add_main_entries (OPTIONS, Config.APP_ID); - opt_context.add_group (Gtk.get_option_group (true)); opt_context.parse (ref args); } catch (OptionError e) { printerr ("error: %s\n", e.message); @@ -101,3 +123,4 @@ public static int main (string[] args) { var app = new Gnonograms.App (); return app.run (args); } +} diff --git a/src/Controller.vala b/src/Controller.vala index 8538f1a..2a72d77 100644 --- a/src/Controller.vala +++ b/src/Controller.vala @@ -1,88 +1,78 @@ -/* Controller.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ + +public const string UNTITLED_NAME = N_("Untitled"); + +[Flags] +public enum SaveFlags { + NONE, + SAVE_STATE, + CONFIRM_OVERWRITE +} + public class Gnonograms.Controller : GLib.Object { - public signal void quit_app (); + public static Controller get_default () { + if (instance == null) { + instance = new Controller (); + instance.set_model_and_view (); + } - private const string SETTINGS_SCHEMA_ID = "com.github.jeremypw.gnonograms.settings"; - private const string SAVED_STATE_SCHEMA_ID = "com.github.jeremypw.gnonograms.saved-state"; + return instance; + } + + private static Controller? instance = null; + private static Gnonograms.App app = (Gnonograms.App) (Application.get_default ()); public Gtk.Window window { get { return (Gtk.Window)view;}} - public GameState game_state { get; set; } - public Dimensions dimensions { get; set; } - public Difficulty generator_grade { get; set; } - public string game_name { get; set; } + // Settings + public string saved_path { get; set; } // Where saved (not temporary file) + public Difficulty generator_grade { get; set; } // Target difficulty of generator. Set in AppPopover + + // Game details + public GameState game_state { get; set; } // Whether solving or designing + public Difficulty game_grade { get; private set; } // Difficulty of the game, if known + public uint rows { get { return dimensions.height; } } // Can be set be App Popover + public uint columns { get { return dimensions.width; } } // Can be set be App Popover + public Dimensions dimensions { get; private set; } + public string game_name { get; set; } // Can be set be App Popover + public string author { get; set; } //TODO Can be set be App Popover - /* Any game that was not saved by this app is regarded as read only - any alterations - * must be "Saved As" - which by default is writable. */ - public bool is_readonly { get; set; default = false;} + // Game states + public bool restart_destructive { get; set; } + public bool can_go_back { + get { + return history.can_go_back; + } + } + public bool can_go_forward { + get { + return history.can_go_forward; + } + } + + // Private members private View view; private Model model; private Solver? solver; private SimpleRandomGameGenerator? generator; - private GLib.Settings? settings = null; - private GLib.Settings? saved_state = null; private Gnonograms.History history; - public string current_game_path { get; private set; default = ""; } - private string saved_games_folder; - private string? temporary_game_path = null; - - construct { - game_name = _(UNTITLED_NAME); - model = new Model (this); - view = new View (model, this); - history = new Gnonograms.History (); - - view.delete_event.connect (on_view_deleted); - view.configure_event.connect (on_view_configure); -#if WITH_DEBUGGING - view.debug_request.connect (on_debug_request); -#endif - notify["game-state"].connect (() => { - if (game_state != GameState.UNDEFINED) { /* Do not clear on save */ - clear_history (); - } - - if (game_state == GameState.GENERATING) { - on_new_random_request (); - } - }); - - notify["dimensions"].connect (() => { - solver = new Solver (dimensions); - game_name = _(UNTITLED_NAME); - }); + private string saved_games_folder; // TODO Make user settable + private string temporary_game_path; - notify["current_game_path"].connect (() => { - view.update_title (); - }); - - if (SettingsSchemaSource.get_default ().lookup (SETTINGS_SCHEMA_ID, true) != null && - SettingsSchemaSource.get_default ().lookup (SAVED_STATE_SCHEMA_ID, true) != null) { + // Signals + public signal void quit_app (); + // public signal void dimensions_changed (uint rows, uint cols); - settings = new Settings (SETTINGS_SCHEMA_ID); - saved_state = new Settings (SAVED_STATE_SCHEMA_ID); - } else { - warning ("not found one of the schemas"); - } + private Controller () {} + construct { + history = new History (); var data_home_folder_current = Path.build_path ( Path.DIR_SEPARATOR_S, @@ -101,39 +91,79 @@ public class Gnonograms.Controller : GLib.Object { saved_games_folder = Environment.get_user_special_dir (UserDirectory.DOCUMENTS); - current_game_path = ""; temporary_game_path = Path.build_path ( Path.DIR_SEPARATOR_S, data_home_folder_current, Gnonograms.UNSAVED_FILENAME ); - if (saved_state != null && settings != null) { - saved_state.bind ("mode", this, "game_state", SettingsBindFlags.DEFAULT); - settings.bind ("grade", this, "generator_grade", SettingsBindFlags.DEFAULT); - settings.bind ("clue-help", view, "strikeout-complete", SettingsBindFlags.DEFAULT); - } + saved_state.bind ("mode", this, "game-state", SettingsBindFlags.DEFAULT); + saved_state.bind ("current-game-path", this, "saved-path", SettingsBindFlags.DEFAULT); + settings.bind ("grade", this, "generator-grade", SettingsBindFlags.DEFAULT); + notify["dimensions"].connect (on_dimensions_changed); + } + + protected void set_model_and_view () { + // Needs to be done after Controller construction complete as they need a controller instance + model = Model.get_default (); + model.changed.connect (() => { + restart_destructive = !model.is_blank (game_state); + }); - view.show_all (); + view = View.get_default (); + view.close_request.connect (() => { + return on_delete_request (); + }); +#if WITH_DEBUGGING + view.debug_request.connect (on_debug_request); +#endif view.present (); - restore_settings (); - bind_property ("generator-grade", view, "generator-grade", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); - bind_property ("is-readonly", view, "readonly", BindingFlags.SYNC_CREATE); + // TODO limit related to actual monitor dimensions + view.default_height = saved_state.get_int ("window-height").clamp (64, 768); + view.default_width = saved_state.get_int ("window-width").clamp (128, 1024); + /* + * This is very finicky. Bind size after present else set_titlebar gives us bad sizes + */ + saved_state.bind ("window-height", view, "default-height", SettingsBindFlags.SET); + saved_state.bind ("window-width", view, "default-width", SettingsBindFlags.SET); + + history.can_go_changed.connect ((forward, back) => { + view.on_can_go_changed (forward, back); + }); - history.bind_property ("can-go-back", view, "can-go-back", BindingFlags.SYNC_CREATE); - history.bind_property ("can-go-forward", view, "can-go-forward", BindingFlags.SYNC_CREATE); + restore_defaults (); restore_game.begin ((obj, res) => { if (!restore_game.end (res)) { /* Error normally thrown if running without installing */ warning ("Restoring game failed"); - restore_dimensions (); + restore_defaults (); new_game (); } }); } + private void restore_defaults () { + var r = settings.get_uint ("rows"); + var c = settings.get_uint ("columns"); + dimensions = { c, r }; + game_grade = Difficulty.UNDEFINED; + game_state = GameState.SETTING; + saved_path = ""; + game_name = _(UNTITLED_NAME); + author = _("Unknown"); + + } + + private void on_dimensions_changed () { + solver = new Solver (dimensions); + game_name = _(UNTITLED_NAME); + settings.set_uint ("rows", dimensions.height); + settings.set_uint ("columns", dimensions.width); + // dimensions_changed (rows, columns); + } + private void new_or_random_game () { if (game_state == GameState.SOLVING && game_name == null) { on_new_random_request (); @@ -142,114 +172,135 @@ public class Gnonograms.Controller : GLib.Object { } } - public void quit () { + public void change_dimensions (uint r, uint c) { + //TODO Check whether OK to change + if (r != rows || c != columns) { + dimensions = { c, r }; + } + } + + public void change_mode (GameState mode) { + switch (mode) { + case SETTING: + case SOLVING: + clear_history (); + game_state = mode; + break; + case GENERATING: + on_new_random_request (); + break; + default: + critical ("Unhandled mode change request"); + break; + } + } + + public void prepare_quit () { if (solver != null) { solver.cancel (); } /* If in middle of generating no defined game to save */ if (generator == null) { - save_game_state (); + app.hold (); + save_game_state.begin ((obj, res) => { + if (!save_game_state.end (res)) { + critical ("Error saving game state"); + } + // Always quit for now + app.release (); + app.quit (); + }); } else { generator.cancel (); + app.quit (); } - - quit_app (); } private void clear () { model.clear (); - view.update_labels_from_solution (); + view.update_clues_from_solution (); clear_history (); - is_readonly = true; // Force Save As when saving new design } private void new_game () { clear (); game_state = GameState.SETTING; game_name = _(UNTITLED_NAME); + saved_path = ""; } private void on_new_random_request () { clear (); solver.cancel (); + author = APP_NAME; + saved_path = ""; + game_name = _("Random pattern"); + game_grade = Difficulty.UNDEFINED; + game_state = GameState.GENERATING; var cancellable = new Cancellable (); solver.cancellable = cancellable; generator = new SimpleRandomGameGenerator (dimensions, solver) { grade = generator_grade }; - game_name = _("Random pattern"); - view.game_grade = Difficulty.UNDEFINED; view.show_working (cancellable, (_("Generating"))); generator.generate.begin ((obj, res) => { var success = generator.generate.end (res); + GameState new_game_state; if (success) { - model.set_solution_from_array (generator.get_solution ()); - game_state = GameState.SOLVING; - view.update_labels_from_solution (); - view.game_grade = generator.solution_grade; + model.set_solution_from_array (generator.get_solution ()); + new_game_state = GameState.SOLVING; + view.update_clues_from_solution (); + game_grade = generator.solution_grade; + } else { + clear (); + new_game_state = GameState.SETTING; + if (cancellable.is_cancelled ()) { + view.send_notification (_("Game generation was cancelled")); } else { - clear (); - game_state = GameState.SETTING; - if (cancellable.is_cancelled ()) { - view.send_notification (_("Game generation was cancelled")); - } else { - view.send_notification (_("Failed to generate game of required grade")); - } + view.send_notification (_("Failed to generate game of required grade")); } + } - view.end_working (); - generator = null; + view.end_working (); + game_state = new_game_state; + + generator = null; }); } - private void save_game_state () { + // Always saved to temp file, not original + private async bool save_game_state () { + string? saved_file_path = null; if (temporary_game_path != null) { - try { - var current_game_file = File.new_for_path (temporary_game_path); - current_game_file.@delete (); - } catch (GLib.Error e) { - /* Error normally thrown on first run */ - debug ("Error deleting temporary game file %s - %s", temporary_game_path, e.message); - } finally { - warning ("writing unsaved game to %s", temporary_game_path); - /* Save solution and current state */ - write_game (temporary_game_path, true); - } - } else { - warning ("No temporary game path"); + saved_file_path = yield write_game (temporary_game_path, SaveFlags.SAVE_STATE); } + + return saved_file_path != null; } - private void restore_settings () { - if (saved_state != null) { - int x, y, cell_size; - saved_state.get ("cell-size", "i", out cell_size); - saved_state.get ("window-position", "(ii)", out x, out y); - current_game_path = saved_state.get_string ("current-game-path"); - view.cell_size = cell_size; - window.move (x, y); + // Called by save action + public async void save_game () { + if (saved_path == "") { + yield save_game_as (); } else { - /* Error normally thrown running uninstalled */ - critical ("Unable to restore settings - using defaults"); /* Maybe running uninstalled */ - /* Default puzzle parameters */ - game_state = GameState.SOLVING; - generator_grade = Difficulty.MODERATE; - current_game_path = ""; + var path = yield write_game (saved_path, SaveFlags.NONE); + if (path != null && path != "") { + saved_path = path; + notify_saved (path); + } } - - restore_dimensions (); } - private void restore_dimensions () { - if (settings != null) { - dimensions = { - settings.get_uint ("columns").clamp (10, 50), - settings.get_uint ("rows").clamp (10, 50) - }; - } else { - dimensions = { 15, 10 }; /* Fallback dimensions */ + // Called by save_as action + public async void save_game_as () { + warning ("Controller: save game as"); + /* Filewriter will request save location, no solution saved as default */ + var path = yield write_game (null, SaveFlags.CONFIRM_OVERWRITE); + if (path != null) { + saved_path = path; + notify_saved (path); } } @@ -262,39 +313,41 @@ public class Gnonograms.Controller : GLib.Object { } } - private string? write_game (string? path, bool save_state = false) { - Filewriter? file_writer = null; - var gs = game_state; + private async string? write_game (string? save_to_path, SaveFlags flags) requires (saved_games_folder != null) { + var file_writer = new Filewriter ( + window, + dimensions, + view.get_clues (false), + view.get_clues (true), + saved_games_folder, + saved_path, + save_to_path + ) { + solution = !model.solution_is_blank () ? model.copy_solution_data () : null, + game_name = this.game_name, + author = this.author, + difficulty = game_grade + }; - game_state = GameState.UNDEFINED; + var gs = game_state; + game_state = LOAD_SAVE; try { - file_writer = new Filewriter (window, - dimensions, - view.get_clues (false), - view.get_clues (true), - history, - !model.solution_is_blank () - ); - - file_writer.difficulty = view.game_grade; - file_writer.game_state = gs; - file_writer.working = model.copy_working_data (); - if (file_writer.save_solution) { - file_writer.solution = model.copy_solution_data (); - } - - file_writer.is_readonly = is_readonly; - - if (save_state) { - file_writer.write_position_file (saved_games_folder, path, game_name); + if (SAVE_STATE in flags) { + yield file_writer.write_position_file ( + model.copy_working_data (), + gs, + history + ); } else { - file_writer.write_game_file (saved_games_folder, path, game_name); + yield file_writer.write_game_file (flags); } - - } catch (IOError e) { + } catch (Error e) { if (!(e is IOError.CANCELLED)) { var basename = Path.get_basename (file_writer.game_path); - Utils.show_error_dialog (_("Unable to save %s").printf (basename), e.message); + Utils.show_error_dialog ( + _("Unable to save %s").printf (basename), + e.message + ); } return null; @@ -308,9 +361,6 @@ public class Gnonograms.Controller : GLib.Object { public void load_game (File? game) { load_game_async.begin (game, (obj, res) => { if (!load_game_async.end (res)) { - warning ("Load game failed"); - current_game_path = ""; - restore_dimensions (); new_or_random_game (); } }); @@ -318,12 +368,15 @@ public class Gnonograms.Controller : GLib.Object { private async bool load_game_async (File? game) { Filereader? reader = null; - var gs = game_state; - - game_state = GameState.UNDEFINED; clear_history (); + reader = new Filereader (); + game_state = LOAD_SAVE; try { - reader = new Filereader (window, Environment.get_user_special_dir (UserDirectory.DOCUMENTS), game); + yield reader.read ( + window, + Environment.get_user_special_dir (UserDirectory.DOCUMENTS), + game + ); } catch (GLib.Error e) { if (!(e is IOError.CANCELLED)) { var basename = game != null ? game.get_basename () : _("game"); @@ -341,21 +394,23 @@ public class Gnonograms.Controller : GLib.Object { } } + game_state = SOLVING; // Default to solving to hide solution return false; - } finally { - game_state = gs; } if (reader.valid && (yield load_common (reader))) { - if (reader.state != GameState.UNDEFINED) { - game_state = reader.state; - } else { - game_state = GameState.SOLVING; + if (reader.has_working) { + model.set_working_data_from_string_array (reader.working[0 : dimensions.height]); } - history.from_string (reader.moves); - if (history.can_go_back) { - make_move (history.get_current_move ()); + if (reader.has_state) { + game_state = reader.state; + history.from_string (reader.moves); + if (history.can_go_back) { + view.make_move (history.get_current_move ()); + } + } else { + game_state = SOLVING; } } else { view.send_notification (_("Unable to load game. %s").printf (reader.err_msg)); @@ -366,6 +421,7 @@ public class Gnonograms.Controller : GLib.Object { } private async bool load_common (Filereader reader) { + game_grade = reader.difficulty; if (reader.has_dimensions) { if (reader.rows > MAXSIZE || reader.cols > MAXSIZE) { reader.err_msg = (_("Dimensions too large")); @@ -374,49 +430,47 @@ public class Gnonograms.Controller : GLib.Object { reader.err_msg = (_("Dimensions too small")); return false; } else { - dimensions = {reader.cols, reader.rows}; + // This will resize model and view as well + dimensions = { reader.cols, reader.rows }; } } else { reader.err_msg = (_("Dimensions missing")); return false; } + if (reader.has_row_clues && reader.has_col_clues) { + view.update_clues_from_string_array (reader.row_clues, false); + view.update_clues_from_string_array (reader.col_clues, true); + } else { + reader.err_msg = (_("Clues missing")); + return false; + } + + if (reader.name.length > 1 && reader.name != "") { + game_name = reader.name; + } + + + if (reader.original_path != null && reader.original_path != "") { + saved_path = reader.original_path; + } else { + saved_path = reader.game_file.get_path (); + } Idle.add (() => { // Need time for model to update dimensions through notify signal model.blank_working (); // Do not reveal solution on load - - if (reader.has_solution) { - view.game_grade = reader.difficulty; - } else if (reader.has_row_clues && reader.has_col_clues) { - view.update_labels_from_string_array (reader.row_clues, false); - view.update_labels_from_string_array (reader.col_clues, true); - } else { - reader.err_msg = (_("Clues missing")); - return false; - } + model.blank_solution (); // Do not reveal solution on load if (reader.has_solution) { model.set_solution_data_from_string_array (reader.solution[0 : dimensions.height]); - view.update_labels_from_solution (); /* Ensure completeness correctly set */ - } - - if (reader.name.length > 1 && reader.name != "") { - game_name = reader.name; - } - - if (reader.has_working) { - model.set_working_data_from_string_array (reader.working[0 : dimensions.height]); + view.update_clues_from_solution (); /* Ensure completeness correctly set */ } + load_common.callback (); return Source.REMOVE; }); - is_readonly = reader.is_readonly; - if (reader.original_path != null && reader.original_path != "") { - current_game_path = reader.original_path; - } else { - current_game_path = reader.game_file.get_path (); - } + yield; return true; } @@ -428,7 +482,6 @@ public class Gnonograms.Controller : GLib.Object { } var errors = model.count_errors (); - while (model.count_errors () > 0 && previous_move ()) { continue; } @@ -447,10 +500,6 @@ public class Gnonograms.Controller : GLib.Object { return errors; } - private void make_move (Move mv) { - view.make_move (mv); - } - private void clear_history () { history.clear_all (); } @@ -464,7 +513,7 @@ public class Gnonograms.Controller : GLib.Object { var moves = solver.hint (row_clues, col_clues, model.copy_working_data ()); foreach (Move mv in moves) { - make_move (mv); + view.make_move (mv); history.record_move (mv.cell, mv.previous_state); } @@ -476,7 +525,7 @@ public class Gnonograms.Controller : GLib.Object { /* Check if puzzle finished */ if (game_state == GameState.SOLVING && !model.solution_is_blank () && model.is_finished) { if (model.count_errors () == 0) { - ///TRANSLATORS: "Correct" is used as an adjective, indicating that a correct (valid) solution has been found. +///TRANSLATORS: "Correct" is used as an adjective, indicating that a correct (valid) solution has been found. view.send_notification (_("Correct solution")); } else if (model.working_matches_clues ()) { view.send_notification (_("Alternative solution found")); @@ -492,7 +541,7 @@ public class Gnonograms.Controller : GLib.Object { public bool next_move () { if (history.can_go_forward) { - make_move (history.pop_next_move ()); + view.make_move (history.pop_next_move ()); return true; } else { return false; @@ -501,62 +550,16 @@ public class Gnonograms.Controller : GLib.Object { public bool previous_move () { if (history.can_go_back) { - make_move (history.pop_previous_move ()); + view.make_move (history.pop_previous_move ()); return true; } else { return false; } } - private bool on_view_deleted () { - quit (); - return false; - } - - private uint configure_id = 0; - private bool on_view_configure () { - if (saved_state == null) { - return false; - } - - if (configure_id != 0) { - GLib.Source.remove (configure_id); - } - - configure_id = Timeout.add (100, () => { - configure_id = 0; - - int x_pos, y_pos; - view.get_position (out x_pos, out y_pos); - saved_state.set ("window-position", "(ii)", x_pos, y_pos); - saved_state.set ("cell-size", "i", view.cell_size); - - return false; - }); - - return false; - } - - public void save_game () { - if (is_readonly || current_game_path == "") { - save_game_as (); - } else { - var path = write_game (current_game_path, false); - if (path != null && path != "") { - current_game_path = path; - notify_saved (path); - } - } - } - - public void save_game_as () { - /* Filewriter will request save location, no solution saved as default */ - var path = write_game (null, false); - if (path != null) { - current_game_path = path; - notify_saved (path); - is_readonly = false; - } + public bool on_delete_request () { + prepare_quit (); // Async + return true; } private void notify_saved (string path) { @@ -568,7 +571,6 @@ public class Gnonograms.Controller : GLib.Object { } public void computer_solve () { - game_state = GameState.SOLVING; start_solving.begin (true); } @@ -600,13 +602,16 @@ public class Gnonograms.Controller : GLib.Object { var moves = solver.debug (idx, is_column, row_clues, col_clues, model.copy_working_data ()); foreach (Move mv in moves) { - make_move (mv); + view.make_move (mv); history.record_move (mv.cell, mv.previous_state); } } #endif - private async SolverState start_solving (bool copy_to_working = false, bool copy_to_solution = false) { + private async SolverState start_solving ( + bool copy_to_working = false, + bool copy_to_solution = false + ) { /* Try as hard as possible to find solution, regardless of grade setting */ var state = SolverState.UNDEFINED; var cancellable = new Cancellable (); @@ -632,18 +637,7 @@ public class Gnonograms.Controller : GLib.Object { view.send_notification (msg); } - view.game_grade = diff; - if (solver.state.solved ()) { - game_state = GameState.SOLVING; - if (copy_to_solution) { - model.copy_to_solution_data (solver.grid); - } - } - - if (copy_to_working) { - model.copy_to_working_data (solver.grid); - } - + game_grade = diff; view.end_working (); return state; } @@ -658,4 +652,7 @@ public class Gnonograms.Controller : GLib.Object { view.end_working (); } + + public void increase_fontsize () {} + public void decrease_fontsize () {} } diff --git a/src/HeaderBar/AppPopover.vala b/src/HeaderBar/AppPopover.vala new file mode 100644 index 0000000..35c1193 --- /dev/null +++ b/src/HeaderBar/AppPopover.vala @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.AppPopover : Gtk.Popover { + private Controller controller = Controller.get_default (); + + construct { + var app = (Gtk.Application)(GLib.Application.get_default ()); + + var title_entry = new Gtk.Entry () { + placeholder_text = _("Enter title of game here"), + margin_top = 12, + }; + + var grade_setting = new Gtk.DropDown.from_strings ( Difficulty.all_human ()); + var grade_preference = new PreferenceRow (_("Degree of difficulty"), grade_setting); + + var row_setting = new Gtk.SpinButton ( + new Gtk.Adjustment (5.0, 5.0, 50.0, 5.0, 5.0, 5.0), + 5.0, + 0 + ) { + snap_to_ticks = true, + orientation = Gtk.Orientation.HORIZONTAL, + width_chars = 3, + }; + + var row_preference = new PreferenceRow (_("Rows"), row_setting); + + var column_setting = new Gtk.SpinButton ( + new Gtk.Adjustment (5.0, 5.0, 50.0, 5.0, 5.0, 5.0), + 5.0, + 0 + ) { + snap_to_ticks = true, + orientation = Gtk.Orientation.HORIZONTAL, + width_chars = 3 + }; + + var column_preference = new PreferenceRow (_("Columns"), column_setting); + //TODO Add Clue help switch + + var load_game_button = new PopoverButton (_("Load"), ACTION_PREFIX + ACTION_OPEN); + var save_game_button = new PopoverButton (_("Save"), ACTION_PREFIX + ACTION_SAVE); + var save_as_game_button = new PopoverButton (_("Save to Different File"), ACTION_PREFIX + ACTION_SAVE_AS); + var preferences_button = new PopoverButton (_("Preferences"), ACTION_PREFIX + ACTION_PREFERENCES); + var shortcut_button = new PopoverButton (_("Keyboard Shortcuts"), ACTION_PREFIX + ACTION_SHORTCUT_WINDOW); + var about_button = new PopoverButton (_("About Gnonograms"), ACTION_PREFIX + ACTION_ABOUT_WINDOW); + + var settings_box = new Gtk.Box (VERTICAL, 3) { + margin_start = 12, + margin_end = 12, + }; + settings_box.append (title_entry); + settings_box.append (grade_preference); + settings_box.append (row_preference); + settings_box.append (column_preference); + settings_box.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + settings_box.append (load_game_button); + settings_box.append (save_game_button); + settings_box.append (save_as_game_button); + settings_box.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + settings_box.append (preferences_button); + settings_box.append (shortcut_button); + settings_box.append (about_button); + + child = settings_box; + + show.connect (() => { + title_entry.text = controller.game_name; + row_setting.value = controller.rows; + column_setting.value = controller.columns; + grade_setting.selected = controller.generator_grade; + }); + + closed.connect (() => { + controller.game_name = title_entry.text; + controller.change_dimensions ((uint) row_setting.value, (uint) column_setting.value); + controller.generator_grade = (Difficulty)(grade_setting.selected); + }); + } +} diff --git a/src/HeaderBar/HeaderBarManager.vala b/src/HeaderBar/HeaderBarManager.vala new file mode 100644 index 0000000..50c9ad8 --- /dev/null +++ b/src/HeaderBar/HeaderBarManager.vala @@ -0,0 +1,222 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + +public class Gnonograms.HeaderBarManager : Object { + + public View view { get; construct; } + + private Gtk.HeaderBar header_bar; + private Gtk.EditableLabel title_label; + private Gtk.Label grade_label; + private Gtk.Stack progress_stack; + private ProgressIndicator progress_indicator; + private Gtk.Button generate_button; + private Gtk.Button undo_button; + private Gtk.Button redo_button; + private Gtk.Button check_correct_button; + private Gtk.Button hint_button; + private Granite.ModeSwitch mode_switch; + private AppPopover app_popover; + private Gtk.Button auto_solve_button; + private Gtk.Button restart_button; + public Difficulty game_grade { + set { + grade_label.label = value.to_string (); + } + } + + private Controller controller = Controller.get_default (); + + public HeaderBarManager (Gnonograms.View view) { + Object ( + view: view + ); + } + + construct { + var app = (Gnonograms.App) Application.get_default (); + header_bar = new Gtk.HeaderBar (); + undo_button = new HeaderButton ( + "edit-undo-symbolic", + ACTION_PREFIX + ACTION_UNDO, + _("Undo Last Move") + ); + redo_button = new HeaderButton ( + "edit-redo-symbolic", + ACTION_PREFIX + ACTION_REDO, + _("Redo Last Move") + ); + check_correct_button = new HeaderButton ( + "media-seek-backward-symbolic", + ACTION_PREFIX + ACTION_CHECK_ERRORS, + _("Check for Errors") + ); + restart_button = new RestartButton ( + "view-refresh-symbolic", + ACTION_PREFIX + ACTION_RESTART, + _("Start again") + ) { + margin_end = 12, + margin_start = 12, + }; + hint_button = new HeaderButton ( + "help-contents-symbolic", + ACTION_PREFIX + ACTION_HINT, + _("Suggest next move") + ); + auto_solve_button = new HeaderButton ( + "computer-symbolic", + ACTION_PREFIX + ACTION_COMPUTER_SOLVE, + _("Check whether design is solvable") + ); + generate_button = new HeaderButton ( + "list-add", + ACTION_PREFIX + ACTION_GENERATING_MODE, + _("Generate New Puzzle") + ); + + app_popover = new AppPopover (); + + var menu_button = new Gtk.MenuButton () { + tooltip_markup = Granite.markup_accel_tooltip ( + app.get_accels_for_action ( + ACTION_PREFIX + ACTION_OPTIONS), + _("Options") + ), + icon_name = "open-menu-symbolic", + valign = Gtk.Align.CENTER, + popover = app_popover + }; + + // Unable to set markup on Granite.ModeSwitch so fake a Granite accelerator tooltip for now. + mode_switch = new Granite.ModeSwitch.from_icon_name ( + "edit-symbolic", + "system-run-symbolic" + ) { + margin_end = 12, + margin_start = 12, + valign = Gtk.Align.CENTER, + primary_icon_tooltip_text = "%s\n%s".printf (_("Edit a Game"), "Ctrl + 1"), + secondary_icon_tooltip_text = "%s\n%s".printf (_("Manually Solve"), "Ctrl + 2") + }; + + mode_switch.notify["active"].connect (() => { + if (mode_switch.active) { + mode_switch.activate_action (ACTION_PREFIX + ACTION_SOLVING_MODE, null); + } else { + mode_switch.activate_action (ACTION_PREFIX + ACTION_SETTING_MODE, null); + } + }); + + progress_indicator = new ProgressIndicator (); + + title_label = new Gtk.EditableLabel ("Gnonograms") { + // use_markup = true, + xalign = 0.5f + }; + title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + + grade_label = new Gtk.Label ("") { + xalign = 0.5f + }; + grade_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + grade_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + var label_box = new Gtk.Box (VERTICAL, 0); + label_box.append (title_label); + label_box.append (grade_label); + + progress_stack = new Gtk.Stack () { + halign = Gtk.Align.CENTER, + hexpand = true + }; + progress_stack.add_named (progress_indicator, "Progress"); + progress_stack.add_named (label_box, "Title"); + progress_stack.set_visible_child_name ("Title"); + progress_stack.add_css_class ("title"); + + header_bar = new Gtk.HeaderBar () { + show_title_buttons = true, + title_widget = progress_stack + }; + + + header_bar.pack_start (generate_button); + header_bar.pack_start (hint_button); + header_bar.pack_start (restart_button); + header_bar.pack_start (undo_button); + header_bar.pack_start (redo_button); + header_bar.pack_start (check_correct_button); + header_bar.pack_end (menu_button); + header_bar.pack_end (mode_switch); + header_bar.pack_end (auto_solve_button); + + controller.bind_property ("game-name", title_label, "text", BIDIRECTIONAL); + controller.bind_property ("saved-path", progress_stack, "tooltip-text", DEFAULT); + controller.bind_property ("game-grade", this, "game-grade", DEFAULT); + + controller.bind_property ( + "restart-destructive", + restart_button, "restart-destructive", + DEFAULT + ); + + } + + public Gtk.HeaderBar get_headerbar () { + return header_bar; + } + + public void on_game_state_changed (GameState gs) { + if (gs == GENERATING) { + generate_button.sensitive = false; + return; + } + + generate_button.sensitive = true; + + + var is_solving = gs == SOLVING; + var is_setting = gs == SETTING; + var sensitive = (is_setting || is_solving); + + mode_switch.active = !is_setting; + mode_switch.sensitive = sensitive; + + hint_button.sensitive = sensitive && is_solving; + auto_solve_button.sensitive = is_setting; + } + + public void popdown_menus () { + app_popover.popdown (); + } + + public void on_can_go_changed (bool forward, bool back) { + check_correct_button.sensitive = back; + undo_button.sensitive = back; + redo_button.sensitive = forward; + } + + public void update_title (string path, Difficulty grade) { + grade_label.label = grade.to_string (); + progress_stack.tooltip_text = path; + progress_stack.set_visible_child_name ("Title"); + } + + public void show_working (string text) { + progress_indicator.text = text; + } + + public void hide_progress () { + progress_stack.set_visible_child_name ("Title"); + } + + public void show_progress (Cancellable? cancellable) { + progress_indicator.cancellable = cancellable; + progress_stack.set_visible_child_name ("Progress"); + } +} diff --git a/src/HeaderBar/HeaderButton.vala b/src/HeaderBar/HeaderButton.vala new file mode 100644 index 0000000..267d6f6 --- /dev/null +++ b/src/HeaderBar/HeaderButton.vala @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + public class Gnonograms.HeaderButton : Gtk.Button { + public HeaderButton (string icon_name, string action_name, string text) { + Object ( + action_name: action_name, + tooltip_markup: Granite.markup_accel_tooltip ( + ((App)(Application.get_default ())).get_accels_for_action (action_name), + text + ), + valign: Gtk.Align.CENTER + ); + + var image = new Gtk.Image.from_icon_name (icon_name) { + pixel_size = 24 + }; + + child = image; + add_css_class ("flat"); + } + } diff --git a/src/HeaderBar/PopoverButton.vala b/src/HeaderBar/PopoverButton.vala new file mode 100644 index 0000000..195f2b6 --- /dev/null +++ b/src/HeaderBar/PopoverButton.vala @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.PopoverButton : Gtk.Button { + public string text { get; construct; } + public string detailed_action { get; construct; } + + public PopoverButton (string _text, string? _action_name = null) { + Object ( + text: _text, + detailed_action: _action_name // Assigning directly to Gtk.Button.action_name doesnt work for some reason + ); + } + + construct { + margin_top = 3; + margin_bottom = 3; + add_css_class (Granite.STYLE_CLASS_FLAT); + set_action_name (detailed_action); + if (text != null && detailed_action != null) { + var accels = ((Gtk.Application) Application.get_default ()).get_accels_for_action (detailed_action); + if (accels != null) { + child = new Granite.AccelLabel (text, accels[0]); + return; + } + } + + child = new Gtk.Label (text); + } +} diff --git a/src/HeaderBar/PreferenceRow.vala b/src/HeaderBar/PreferenceRow.vala new file mode 100644 index 0000000..824fd13 --- /dev/null +++ b/src/HeaderBar/PreferenceRow.vala @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + +public class Gnonograms.PreferenceRow : Gtk.Box { + public string text { get; construct; } + public Gtk.Widget widget { get; construct; } + public PreferenceRow (string text, Gtk.Widget setting_widget) { + Object ( + text: text, + widget: setting_widget + ); + } + + construct { + orientation = Gtk.Orientation.HORIZONTAL; + margin_top = 3; + margin_bottom = 6; + spacing = 12; + hexpand = true; + + var label = new Gtk.Label (text) { + halign = Gtk.Align.START + }; + + widget.halign = Gtk.Align.END; + widget.hexpand = true; + + append (label); + append (widget); + } +} diff --git a/src/HeaderBar/ProgressIndicator.vala b/src/HeaderBar/ProgressIndicator.vala new file mode 100644 index 0000000..8f99de3 --- /dev/null +++ b/src/HeaderBar/ProgressIndicator.vala @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + public class Gnonograms.ProgressIndicator : Gtk.Box { + private Gtk.Spinner spinner; + private Gtk.Button cancel_button; + private Gtk.Label label; + + public string text { + set { + label.label = value; + } + } + + public Cancellable? cancellable { get; set; } + + public ProgressIndicator () { + Object ( + orientation: Gtk.Orientation.HORIZONTAL, + homogeneous: false, + spacing: 6, + valign: Gtk.Align.CENTER + ); + } + + construct { + spinner = new Gtk.Spinner (); + label = new Gtk.Label (null); + label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); + + append (label); + append (spinner); + + cancel_button = new Gtk.Button (); + var img = new Gtk.Image.from_icon_name ("process-stop-symbolic") { + tooltip_text = _("Cancel solving") + }; + cancel_button.child = img; + cancel_button.add_css_class ("warn"); + cancel_button.add_css_class ("flat"); + img.add_css_class ("warn"); + + append (cancel_button); + + cancel_button.clicked.connect (() => { + if (cancellable != null) { + cancellable.cancel (); + } + }); + + realize.connect (() => { + if (cancellable != null) { + cancel_button.show (); + } + + spinner.start (); + }); + + unrealize.connect (() => { + if (cancellable != null) { + cancellable = null; + cancel_button.hide (); + } + + spinner.stop (); + }); + } + } diff --git a/src/HeaderBar/RestartButton.vala b/src/HeaderBar/RestartButton.vala new file mode 100644 index 0000000..6d7337f --- /dev/null +++ b/src/HeaderBar/RestartButton.vala @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + private class Gnonograms.RestartButton : Gnonograms.HeaderButton { + public bool restart_destructive { get; set; } + + construct { + notify["restart-destructive"].connect (() => { + if (restart_destructive) { + add_css_class ("warn"); + remove_css_class ("dim"); + } else { + remove_css_class ("warn"); + add_css_class ("dim"); + } + }); + + bind_property ("sensitive", this, "restart-destructive"); + } + + public RestartButton (string icon_name, string action_name, string text) { + base (icon_name, action_name, text); + } + } diff --git a/libcore/Model.vala b/src/Model.vala similarity index 75% rename from libcore/Model.vala rename to src/Model.vala index 957d110..27b0bbe 100644 --- a/libcore/Model.vala +++ b/src/Model.vala @@ -1,22 +1,20 @@ -/* Model.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ public class Gnonograms.Model : GLib.Object { + public static Model get_default () { + if (instance == null) { + instance = new Model (); + } + + return instance; + } + + private static Model? instance; + public signal void changed (); public My2DCellArray display_data { @@ -31,40 +29,49 @@ public class Gnonograms.Model : GLib.Object { } } - public Controller controller { get; construct; } + private Controller controller = Controller.get_default (); + private My2DCellArray solution_data { get; set; } private My2DCellArray working_data { get; set; } - private uint rows = 0; - private uint cols = 0; - - public Model (Controller controller) { - Object ( - controller: controller - ); + private uint rows = 5; + private uint cols = 5; + private Dimensions dimensions { + get { + return { cols, rows }; + } } - construct { - controller.notify["dimensions"].connect (() => { - rows = controller.dimensions.height; - cols = controller.dimensions.width; - solution_data = new My2DCellArray (controller.dimensions, CellState.EMPTY); - working_data = new My2DCellArray (controller.dimensions, CellState.UNKNOWN); - changed (); - }); + private Model () {} + construct { + make_data_arrays (); + controller.notify["dimensions"].connect (on_dimensions_changed); controller.notify["game-state"].connect (() => { changed (); }); } + private void on_dimensions_changed () { + this.rows = controller.rows; + this.cols = controller.columns; + make_data_arrays (); + } + + private void make_data_arrays () { + solution_data = new My2DCellArray (dimensions, CellState.EMPTY); + working_data = new My2DCellArray (dimensions, CellState.UNKNOWN); + } + public int count_errors () { CellState cs; int count = 0; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { cs = working_data.get_data_from_rc (r, c); - if (cs != CellState.UNKNOWN && cs != solution_data.get_data_from_rc (r, c)) { + if (cs != CellState.UNKNOWN && + cs != solution_data.get_data_from_rc (r, c) + ) { count++; } } @@ -99,7 +106,10 @@ public class Gnonograms.Model : GLib.Object { public bool is_blank (GameState state) { if (state == GameState.SOLVING) { - return count_state (state, CellState.EMPTY) + count_state (state, CellState.FILLED) == 0; + var non_blank = count_state (state, CellState.EMPTY) + + count_state (state, CellState.FILLED); + + return non_blank == 0; } else { return count_state (state, CellState.FILLED) == 0; } @@ -155,7 +165,10 @@ public class Gnonograms.Model : GLib.Object { return working_data.data2text (idx, length, is_column); } - public Gee.ArrayList get_complete_blocks_from_working (uint index, bool is_column) { + public Gee.ArrayList get_complete_blocks_from_working ( + uint index, + bool is_column + ) { var csa = new CellState[is_column ? rows : cols]; working_data.get_array (index, is_column, ref csa); return Utils.complete_block_array_from_cellstate_array (csa); @@ -201,7 +214,10 @@ public class Gnonograms.Model : GLib.Object { return display_data.get_data_from_rc (r, c); } - private void set_row_data_from_string_array (string[] row_data_strings, My2DCellArray array) { + private void set_row_data_from_string_array ( + string[] row_data_strings, + My2DCellArray array + ) { assert (row_data_strings.length == rows); int row = 0; foreach (var row_string in row_data_strings) { @@ -231,13 +247,13 @@ public class Gnonograms.Model : GLib.Object { } public My2DCellArray copy_working_data () { - var grid = new My2DCellArray (controller.dimensions, CellState.UNKNOWN); + var grid = new My2DCellArray (dimensions, CellState.UNKNOWN); grid.copy (working_data); return grid; } public My2DCellArray copy_solution_data () { - var grid = new My2DCellArray (controller.dimensions, CellState.UNKNOWN); + var grid = new My2DCellArray (dimensions, CellState.UNKNOWN); grid.copy (solution_data); return grid; } diff --git a/src/View.vala b/src/View.vala index efe04d3..cf93dfe 100644 --- a/src/View.vala +++ b/src/View.vala @@ -1,350 +1,30 @@ -/* View.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ -public class Gnonograms.View : Hdy.ApplicationWindow { - private class ProgressIndicator : Gtk.Grid { - private Gtk.Spinner spinner; - private Gtk.Button cancel_button; - private Gtk.Label label; - - public string text { - set { - label.label = value; - } - } - - public Cancellable? cancellable { get; set; } - - public ProgressIndicator () { - Object ( - orientation: Gtk.Orientation.HORIZONTAL, - column_homogeneous: false, - column_spacing: 6, - valign: Gtk.Align.CENTER - ); +public class Gnonograms.View : Gtk.ApplicationWindow { + public static View get_default () { + if (instance == null) { + instance = new View (); } - construct { - spinner = new Gtk.Spinner (); - label = new Gtk.Label (null); - label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); - - add (label); - add (spinner); - - cancel_button = new Gtk.Button (); - var img = new Gtk.Image.from_icon_name ("process-stop-symbolic", Gtk.IconSize.LARGE_TOOLBAR); - img.set_tooltip_text (_("Cancel solving")); - cancel_button.image = img; - cancel_button.no_show_all = true; - cancel_button.get_style_context ().add_class ("warn"); - img.get_style_context ().add_class ("warn"); - - add (cancel_button); - - show_all (); - - cancel_button.clicked.connect (() => { - if (cancellable != null) { - cancellable.cancel (); - } - }); - - realize.connect (() => { - if (cancellable != null) { - cancel_button.show (); - } - - spinner.start (); - }); - - unrealize.connect (() => { - if (cancellable != null) { - cancellable = null; - cancel_button.hide (); - } - - spinner.stop (); - }); - } + return instance; } - private class HeaderButton : Gtk.Button { - construct { - valign = Gtk.Align.CENTER; - } - - public HeaderButton (string icon_name, string action_name, string text) { - Object ( - action_name: action_name, - tooltip_markup: Granite.markup_accel_tooltip ( - View.app.get_accels_for_action (action_name), text), - image: new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.LARGE_TOOLBAR) - ); - } - } - - private class RestartButton : HeaderButton { - public bool restart_destructive { get; set; } - - construct { - restart_destructive = false; - - notify["restart-destructive"].connect (() => { - if (restart_destructive) { - image.get_style_context ().add_class ("warn"); - image.get_style_context ().remove_class ("dim"); - } else { - image.get_style_context ().remove_class ("warn"); - image.get_style_context ().add_class ("dim"); - - } - }); - - bind_property ("sensitive", this, "restart-destructive"); - } - - public RestartButton (string icon_name, string action_name, string text) { - base (icon_name, action_name, text); - } - } - - private class AppMenu : Gtk.MenuButton { - private class GradeChooser : Gtk.ComboBoxText { - public Difficulty grade { - get { - return (Difficulty)(int.parse (active_id)); - } - - set { - active_id = ((uint)value).clamp (MIN_GRADE, Difficulty.MAXIMUM).to_string (); - } - } - - public GradeChooser () { - Object ( - expand: false - ); - - foreach (Difficulty d in Difficulty.all_human ()) { - append (((uint)d).to_string (), d.to_string ()); - } - } - } - - private class DimensionSpinButton : Gtk.SpinButton { - public DimensionSpinButton () { - Object ( - adjustment: new Gtk.Adjustment (5.0, 5.0, 50.0, 5.0, 5.0, 5.0), - climb_rate: 5.0, - digits: 0, - snap_to_ticks: true, - orientation: Gtk.Orientation.HORIZONTAL, - margin_top: 3, - margin_bottom: 3, - width_chars: 3, - can_focus: true - ); - } - } - - public unowned Controller controller { get; construct; } - - public AppMenu (Controller controller) { - Object ( - image: new Gtk.Image.from_icon_name ("open-menu", Gtk.IconSize.LARGE_TOOLBAR), - tooltip_text: _("Options"), - controller: controller - ); - } - - construct { - var zoom_out_button = new Gtk.Button.from_icon_name ("zoom-out-symbolic", Gtk.IconSize.MENU) { - action_name = ACTION_PREFIX + ACTION_ZOOM_OUT, - tooltip_markup = Granite.markup_accel_tooltip ( - app.get_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_OUT), _("Zoom out") - ) - }; - - var zoom_in_button = new Gtk.Button.from_icon_name ("zoom-in-symbolic", Gtk.IconSize.MENU) { - action_name = ACTION_PREFIX + ACTION_ZOOM_IN, - tooltip_markup = Granite.markup_accel_tooltip ( - app.get_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_IN), _("Zoom in") - ) - }; - - var size_grid = new Gtk.Grid () { - column_homogeneous = true, - hexpand = true, - margin= 12 - }; - size_grid.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED); - - size_grid.add (zoom_out_button); - size_grid.add (zoom_in_button); - - var grade_setting = new GradeChooser (); - var row_setting = new DimensionSpinButton (); - var column_setting = new DimensionSpinButton (); - var title_setting = new Gtk.Entry () { - placeholder_text = _("Enter title of game here") - }; - - var settings_grid = new Gtk.Grid () { - orientation = Gtk.Orientation.VERTICAL, - margin = 12, - row_spacing = 6, - column_homogeneous = false - }; - settings_grid.attach (new SettingLabel (_("Name:")), 0, 0, 1); - settings_grid.attach (title_setting, 1, 0, 3); - settings_grid.attach (new SettingLabel (_("Difficulty:")), 0, 1, 1); - settings_grid.attach (grade_setting, 1, 1, 3); - settings_grid.attach (new SettingLabel (_("Rows:")), 0, 2, 1); - settings_grid.attach (row_setting, 1, 2, 1); - settings_grid.attach (new SettingLabel (_("Columns:")), 0, 3, 1); - settings_grid.attach (column_setting, 1, 3, 1); - - var main_grid = new Gtk.Grid () {orientation = Gtk.Orientation.VERTICAL}; - main_grid.add (size_grid); - main_grid.add (settings_grid); - - var app_popover = new AppPopover (); - app_popover.add (main_grid); - set_popover (app_popover); - - app_popover.apply_settings.connect (() => { - controller.generator_grade = grade_setting.grade; - controller.dimensions = {(uint)column_setting.@value, (uint)row_setting.@value}; - controller.game_name = title_setting.text; // Must come after changing dimensions - }); + private static View? instance = null; - toggled.connect (() => { /* Allow parent to set values first */ - if (active) { - grade_setting.grade = controller.generator_grade; - row_setting.value = (double)(controller.dimensions.height); - column_setting.value = (double)(controller.dimensions.width); - title_setting.text = controller.game_name; - popover.show_all (); - } - }); - } - - /** Popover that can be cancelled with Escape and closed by Enter **/ - private class AppPopover : Gtk.Popover { - private bool cancelled = false; - public signal void apply_settings (); - public signal void cancel (); - - construct { - closed.connect (() => { - if (!cancelled) { - apply_settings (); - } else { - cancel (); - } - - cancelled = false; - }); - - key_press_event.connect ((event) => { - cancelled = (event.keyval == Gdk.Key.Escape); - - if (event.keyval == Gdk.Key.KP_Enter || event.keyval == Gdk.Key.Return) { - hide (); - } - }); - } - } - - private class SettingLabel : Gtk.Label { - public SettingLabel (string text) { - Object ( - label: text, - xalign: 1.0f, - margin_end: 6 - ); - } - } - } - - private const double USABLE_MONITOR_HEIGHT = 0.85; - private const double USABLE_MONITOR_WIDTH = 0.95; - private const int GRID_BORDER = 6; - private const int GRID_COLUMN_SPACING = 6; - private const double TYPICAL_MAX_BLOCKS_RATIO = 0.3; - private const double ZOOM_RATIO = 0.05; private const uint PROGRESS_DELAY_MSEC = 500; + private const int DEFAULT_WIDTH = 900; + private const int DEFAULT_HEIGHT = 700; + private const uint DARK = Granite.Settings.ColorScheme.DARK; -#if WITH_DEBUGGING - public signal void debug_request (uint idx, bool is_column); -#endif - - public Model model { get; construct; } - public Controller controller { get; construct; } - public Cell current_cell { get; set; } - public Cell previous_cell { get; set; } - public Difficulty generator_grade { get; set; } - public Difficulty game_grade { get; set; default = Difficulty.UNDEFINED;} - public int cell_size { get; set; default = 32; } - public string game_name { get { return controller.game_name; } } - public bool strikeout_complete { get; set; } - public bool readonly { get; set; default = false;} - public bool can_go_back { get; set; } - public bool can_go_forward { get; set; } - public bool restart_destructive { get; set; default = false;} - - public SimpleActionGroup view_actions { get; construct; } - public static Gee.MultiMap action_accelerators = new Gee.HashMultiMap (); - public const string ACTION_GROUP = "win"; - public const string ACTION_PREFIX = ACTION_GROUP + "."; - public const string ACTION_UNDO = "action-undo"; - public const string ACTION_REDO = "action-redo"; - public const string ACTION_ZOOM_IN = "action-zoom-in"; - public const string ACTION_ZOOM_OUT = "action-zoom-out"; - public const string ACTION_CURSOR_UP = "action-cursor_up"; - public const string ACTION_CURSOR_DOWN = "action-cursor_down"; - public const string ACTION_CURSOR_LEFT = "action-cursor_left"; - public const string ACTION_CURSOR_RIGHT = "action-cursor_right"; - public const string ACTION_SETTING_MODE = "action-setting-mode"; - public const string ACTION_SOLVING_MODE = "action-solving-mode"; - public const string ACTION_GENERATING_MODE = "action-generating-mode"; - public const string ACTION_OPEN = "action-open"; - public const string ACTION_SAVE = "action-save"; - public const string ACTION_SAVE_AS = "action-save-as"; - public const string ACTION_PAINT_FILLED = "action-paint-filled"; - public const string ACTION_PAINT_EMPTY = "action-paint-empty"; - public const string ACTION_PAINT_UNKNOWN = "action-paint-unknown"; - public const string ACTION_CHECK_ERRORS = "action-check-errors"; - public const string ACTION_RESTART = "action-restart"; - public const string ACTION_SOLVE = "action-solve"; - public const string ACTION_HINT = "action-hint"; - public const string ACTION_OPTIONS = "action-options"; -#if WITH_DEBUGGING - public const string ACTION_DEBUG_ROW = "action-debug-row"; - public const string ACTION_DEBUG_COL = "action-debug-col"; -#endif + public static Gee.MultiMap action_accelerators; private static GLib.ActionEntry [] view_action_entries = { {ACTION_UNDO, action_undo}, {ACTION_REDO, action_redo}, - {ACTION_ZOOM_IN, action_zoom_in}, - {ACTION_ZOOM_OUT, action_zoom_out}, {ACTION_CURSOR_UP, action_cursor_up}, {ACTION_CURSOR_DOWN, action_cursor_down}, {ACTION_CURSOR_LEFT, action_cursor_left}, @@ -355,58 +35,59 @@ public class Gnonograms.View : Hdy.ApplicationWindow { {ACTION_OPEN, action_open}, {ACTION_SAVE, action_save}, {ACTION_SAVE_AS, action_save_as}, - {ACTION_PAINT_FILLED, action_paint_filled}, - {ACTION_PAINT_EMPTY, action_paint_empty}, - {ACTION_PAINT_UNKNOWN, action_paint_unknown}, {ACTION_CHECK_ERRORS, action_check_errors}, {ACTION_RESTART, action_restart}, - {ACTION_SOLVE, action_solve}, + {ACTION_COMPUTER_SOLVE, action_computer_solve}, {ACTION_HINT, action_hint}, - {ACTION_OPTIONS, action_options} + {ACTION_OPTIONS, action_options}, + {ACTION_PREFERENCES, action_preferences}, + {ACTION_ZOOM_SMALLER, action_zoom_smaller}, + {ACTION_ZOOM_DEFAULT, action_zoom_default}, + {ACTION_ZOOM_LARGER, action_zoom_larger}, + {ACTION_SHORTCUT_WINDOW, action_shortcut_window}, + {ACTION_ABOUT_WINDOW, action_about_dialog} }; - public static Gtk.Application app; - private LabelBox row_clue_box; - private LabelBox column_clue_box; +#if WITH_DEBUGGING + public signal void debug_request (uint idx, bool is_column); +#endif + + public SimpleActionGroup view_actions { get; construct; } + + public Cell? current_cell { get; set; } + public Cell? previous_cell { get; set; } + // public bool restart_destructive { get; set; default = false;} + + private Controller controller = Controller.get_default (); + private Model model = Model.get_default (); + private ClueBox row_clue_box; + private ClueBox column_clue_box; private CellGrid cell_grid; - private ProgressIndicator progress_indicator; - private AppMenu app_menu; - private CellState drawing_with_state = CellState.UNDEFINED; - private Hdy.HeaderBar header_bar; - private Granite.Widgets.Toast toast; - private Granite.ModeSwitch mode_switch; + private Gtk.MenuButton menu_button; + private HeaderBarManager headerbar_manager; private Gtk.Grid main_grid; - private Gtk.Overlay overlay; - private Gtk.Stack progress_stack; - private Gtk.Label title_label; - private Gtk.Label grade_label; - private Gtk.Button generate_button; - private Gtk.Button load_game_button; - private Gtk.Button save_game_button; - private Gtk.Button save_game_as_button; - private Gtk.Button undo_button; - private Gtk.Button redo_button; - private Gtk.Button check_correct_button; - private Gtk.Button hint_button; - private Gtk.Button auto_solve_button; - private Gtk.Button restart_button; - private uint drawing_with_key; - - public View (Model _model, Controller controller) { - Object ( - model: _model, - controller: controller, - resizable: false, - title: _("Gnonograms") - ); - } + private Adw.ToastOverlay toast_overlay; + private uint drawing_with_key = 0; + private CellState drawing_with_state = INVALID; + private uint paint_fill_key = Gdk.keyval_from_name ("f"); + private uint paint_empty_key = Gdk.keyval_from_name ("e"); + private uint paint_unknown_key = Gdk.keyval_from_name ("x"); + + private View () {} static construct { - app = (Gtk.Application)(Application.get_default ()); + action_accelerators = new Gee.HashMultiMap (); + #if WITH_DEBUGGING -warning ("WITH DEBUGGING"); - view_action_entries += ActionEntry () { name = ACTION_DEBUG_ROW, activate = action_debug_row }; - view_action_entries += ActionEntry () { name = ACTION_DEBUG_COL, activate = action_debug_col }; + warning ("WITH DEBUGGING"); + view_action_entries += ActionEntry () { + name = ACTION_DEBUG_ROW, + activate = action_debug_row + }; + view_action_entries += ActionEntry () { + name = ACTION_DEBUG_COL, + activate = action_debug_col + }; #endif action_accelerators.set (ACTION_UNDO, "Z"); action_accelerators.set (ACTION_REDO, "Z"); @@ -414,11 +95,6 @@ warning ("WITH DEBUGGING"); action_accelerators.set (ACTION_CURSOR_DOWN, "Down"); action_accelerators.set (ACTION_CURSOR_LEFT, "Left"); action_accelerators.set (ACTION_CURSOR_RIGHT, "Right"); - action_accelerators.set (ACTION_ZOOM_IN, "plus"); - action_accelerators.set (ACTION_ZOOM_IN, "equal"); - action_accelerators.set (ACTION_ZOOM_IN, "KP_Add"); - action_accelerators.set (ACTION_ZOOM_OUT, "minus"); - action_accelerators.set (ACTION_ZOOM_OUT, "KP_Subtract"); action_accelerators.set (ACTION_SETTING_MODE, "1"); action_accelerators.set (ACTION_SOLVING_MODE, "2"); action_accelerators.set (ACTION_GENERATING_MODE, "3"); @@ -426,44 +102,36 @@ warning ("WITH DEBUGGING"); action_accelerators.set (ACTION_OPEN, "O"); action_accelerators.set (ACTION_SAVE, "S"); action_accelerators.set (ACTION_SAVE_AS, "S"); - action_accelerators.set (ACTION_PAINT_FILLED, "F"); - action_accelerators.set (ACTION_PAINT_EMPTY, "E"); - action_accelerators.set (ACTION_PAINT_UNKNOWN, "X"); action_accelerators.set (ACTION_CHECK_ERRORS, "F7"); action_accelerators.set (ACTION_RESTART, "F5"); action_accelerators.set (ACTION_RESTART, "R"); action_accelerators.set (ACTION_HINT, "F9"); action_accelerators.set (ACTION_HINT, "H"); - action_accelerators.set (ACTION_SOLVE, "S"); + action_accelerators.set (ACTION_COMPUTER_SOLVE, "S"); action_accelerators.set (ACTION_OPTIONS, "F10"); action_accelerators.set (ACTION_OPTIONS, "Menu"); + action_accelerators.set (ACTION_PREFERENCES, "P"); + action_accelerators.set (ACTION_SHORTCUT_WINDOW, "K"); + action_accelerators.set (ACTION_SHORTCUT_WINDOW, "F1"); + action_accelerators.set (ACTION_ZOOM_LARGER, "plus"); + action_accelerators.set (ACTION_ZOOM_LARGER, "equal"); + action_accelerators.set (ACTION_ZOOM_DEFAULT, "0"); + action_accelerators.set (ACTION_ZOOM_SMALLER, "minus"); #if WITH_DEBUGGING action_accelerators.set (ACTION_DEBUG_ROW, "R"); action_accelerators.set (ACTION_DEBUG_COL, "C"); #endif - try { - var css_provider = new Gtk.CssProvider (); - css_provider.load_from_resource ("com/github/jeremypw/gnonograms/Application.css"); - Gtk.StyleContext.add_provider_for_screen ( - Gdk.Screen.get_default (), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); - } catch (Error e) { - warning ("Error adding css provider: %s", e.message); - } - - Hdy.init (); } construct { - weak Gtk.IconTheme default_theme = Gtk.IconTheme.get_default (); - default_theme.add_resource_path ("/com/github/jeremypw/gnonograms"); - + var app = (Gnonograms.App) Application.get_default (); + title = _("Gnonograms"); + set_default_size (DEFAULT_WIDTH, DEFAULT_HEIGHT); var view_actions = new GLib.SimpleActionGroup (); view_actions.add_action_entries (view_action_entries, this); insert_action_group (ACTION_GROUP, view_actions); - foreach (var action in action_accelerators.get_keys ()) { var accels_array = action_accelerators[action].to_array (); accels_array += null; @@ -471,373 +139,226 @@ warning ("WITH DEBUGGING"); app.set_accels_for_action (ACTION_PREFIX + action, accels_array); } - load_game_button = new HeaderButton ("document-open", ACTION_PREFIX + ACTION_OPEN, _("Load Game")); - save_game_button = new HeaderButton ("document-save", ACTION_PREFIX + ACTION_SAVE, _("Save Game")); - save_game_as_button = new HeaderButton ("document-save-as", ACTION_PREFIX + ACTION_SAVE_AS, _("Save Game to Different File")); - undo_button = new HeaderButton ("edit-undo", ACTION_PREFIX + ACTION_UNDO, _("Undo Last Move")); - redo_button = new HeaderButton ("edit-redo", ACTION_PREFIX + ACTION_REDO, _("Redo Last Move")); - check_correct_button = new HeaderButton ("media-seek-backward", ACTION_PREFIX + ACTION_CHECK_ERRORS, _("Check for Errors")); - restart_button = new RestartButton ("view-refresh", ACTION_PREFIX + ACTION_RESTART, _("Start again")) { - margin_end = 12, - margin_start = 12, - }; - hint_button = new HeaderButton ("help-contents", ACTION_PREFIX + ACTION_HINT, _("Suggest next move")); - auto_solve_button = new HeaderButton ("system", ACTION_PREFIX + ACTION_SOLVE, _("Solve by Computer")); - generate_button = new HeaderButton ("list-add", ACTION_PREFIX + ACTION_GENERATING_MODE, _("Generate New Puzzle")); - app_menu = new AppMenu (controller) { - tooltip_markup = Granite.markup_accel_tooltip (app.get_accels_for_action (ACTION_PREFIX + ACTION_OPTIONS), _("Options")) - }; - - // Unable to set markup on Granite.ModeSwitch so fake a Granite acellerator tooltip for now. - mode_switch = new Granite.ModeSwitch.from_icon_name ("edit-symbolic", "head-thinking-symbolic") { - margin_end = 12, - margin_start = 12, - valign = Gtk.Align.CENTER, - primary_icon_tooltip_text = "%s\n%s".printf (_("Edit a Game"), "Ctrl + 1"), - secondary_icon_tooltip_text = "%s\n%s".printf (_("Manually Solve"), "Ctrl + 2") - }; + headerbar_manager = new HeaderBarManager (this); - progress_indicator = new ProgressIndicator (); + set_titlebar (headerbar_manager.get_headerbar ()); - title_label = new Gtk.Label ("Gnonograms") { - use_markup = true, - xalign = 0.5f - }; - title_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); - title_label.show (); + row_clue_box = new ClueBox (false); + column_clue_box = new ClueBox (true); + cell_grid = new CellGrid (); - grade_label = new Gtk.Label ("Easy") { - use_markup = true, - xalign = 0.5f - }; - grade_label.get_style_context ().add_class (Granite.STYLE_CLASS_H4_LABEL); - - var title_grid = new Gtk.Grid () { - orientation = Gtk.Orientation.VERTICAL - }; - title_grid.add (title_label); - title_grid.add (grade_label); - title_grid.show_all (); - - progress_stack = new Gtk.Stack (); - progress_stack.add_named (progress_indicator, "Progress"); - progress_stack.add_named (title_grid, "Title"); - progress_stack.set_visible_child_name ("Title"); - - header_bar = new Hdy.HeaderBar () { - has_subtitle = false, - show_close_button = true, - custom_title = progress_stack - }; - header_bar.get_style_context ().add_class ("gnonograms-header"); - header_bar.pack_start (load_game_button); - header_bar.pack_start (save_game_button); - header_bar.pack_start (save_game_as_button); - header_bar.pack_start (restart_button); - header_bar.pack_start (undo_button); - header_bar.pack_start (redo_button); - header_bar.pack_start (check_correct_button); - header_bar.pack_end (app_menu); - header_bar.pack_end (generate_button); - header_bar.pack_end (mode_switch); - header_bar.pack_end (auto_solve_button); - header_bar.pack_end (hint_button); - - toast = new Granite.Widgets.Toast ("") { - halign = Gtk.Align.START, - valign = Gtk.Align.START - }; - toast.set_default_action (null); - - row_clue_box = new LabelBox (Gtk.Orientation.VERTICAL, this); - column_clue_box = new LabelBox (Gtk.Orientation.HORIZONTAL, this); - cell_grid = new CellGrid (this); + cell_grid.bind_property ("cell-width", column_clue_box, "cell-size"); + cell_grid.bind_property ("cell-height", row_clue_box, "cell-size"); main_grid = new Gtk.Grid () { - row_spacing = 0, - column_spacing = GRID_COLUMN_SPACING, - border_width = GRID_BORDER, - expand = true + focusable = true, // Needed for key controller to work + row_spacing = 6, + column_spacing = 6, + margin_start = 6, + margin_end = 6, + margin_top = 6, + margin_bottom = 6 }; - main_grid.attach (row_clue_box, 0, 1, 1, 1); /* Clues fordimensions.height*/ + + main_grid.attach (row_clue_box, 0, 1, 1, 1); /* Clues for dimensions.height*/ main_grid.attach (column_clue_box, 1, 0, 1, 1); /* Clues for columns */ main_grid.attach (cell_grid, 1, 1, 1, 1); - var ev = new Gtk.EventBox () {expand = true}; - ev.add_events (Gdk.EventMask.SCROLL_MASK); - ev.scroll_event.connect (on_grid_scroll_event); - ev.add (main_grid); - - overlay = new Gtk.Overlay () { - expand = true + toast_overlay = new Adw.ToastOverlay () { + child = main_grid }; - overlay.add_overlay (toast); - overlay.add (ev); - var grid = new Gtk.Grid () { - orientation = Gtk.Orientation.VERTICAL - }; - grid.add (header_bar); - grid.add (overlay); - add (grid); + child = toast_overlay; - var flags = BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE; - bind_property ("restart-destructive", restart_button, "restart-destructive", BindingFlags.SYNC_CREATE); - bind_property ("current-cell", cell_grid, "current-cell", BindingFlags.BIDIRECTIONAL); - bind_property ("previous-cell", cell_grid, "previous-cell", BindingFlags.BIDIRECTIONAL); + var key_controller = new Gtk.EventControllerKey (); + main_grid.add_controller (key_controller); - mode_switch.notify["active"].connect (() => { - controller.game_state = mode_switch.active ? GameState.SOLVING : GameState.SETTING; + key_controller.key_pressed.connect ((keyval, keycode, state) => { + if (keyval == paint_fill_key) { + paint_filled (); + } else if (keyval == paint_empty_key) { + paint_empty (); + } else if (keyval == paint_unknown_key) { + paint_unknown (); + } else { + return false; + } + + return true; }); - controller.notify["game-state"].connect (() => { - if (controller.game_state != GameState.UNDEFINED) { - update_all_labels_completeness (); + key_controller.key_released.connect ((keyval, keycode, state) => { + if (keyval == drawing_with_key) { + stop_painting (); } + }); - // Avoid updating header bar while generating otherwise generation will be cancelled. - // Headerbar will update when generation finished. - if (controller.game_state != GameState.GENERATING) { - update_header_bar (); + var button_controller = new Gtk.GestureClick (); + button_controller.set_button (0); // Listen to any button + main_grid.add_controller (button_controller); + button_controller.pressed.connect ((n_press, x, y) => { + var button = button_controller.get_current_button (); + var shift = (SHIFT_MASK in button_controller.get_current_event_state ()); + var set_unknown = (n_press == 2 || button == Gdk.BUTTON_MIDDLE); + var set_empty = (button == Gdk.BUTTON_SECONDARY || button == Gdk.BUTTON_PRIMARY && shift); + + if (set_unknown) { // Clear current cell + drawing_with_state = controller.game_state == SOLVING ? CellState.UNKNOWN : CellState.EMPTY; + } else { // Paint current cell + drawing_with_state = set_empty ? CellState.EMPTY : CellState.FILLED; } - }); - controller.notify["game-name"].connect (() => { - update_title (); + make_move_at_cell (); }); - - notify["game-grade"].connect (() => { - update_title (); + button_controller.released.connect ((n_press, x, y) => { + stop_painting (); }); - notify["readonly"].connect (() => { - save_game_button.sensitive = readonly; - }); + current_cell = Cell () { row = 0, col = 0, state = UNKNOWN }; + previous_cell = current_cell.clone (); - notify["can-go-back"].connect (() => { - check_correct_button.sensitive = can_go_back && controller.game_state == GameState.SOLVING; - undo_button.sensitive = can_go_back; - restart_destructive |= can_go_back; /* May be destructive even if no history (e.g. after automatic solve) */ - }); + bind_property ( + "current-cell", + cell_grid, "current-cell", + BindingFlags.BIDIRECTIONAL + ); + bind_property ( + "previous-cell", + cell_grid, "previous-cell", + BindingFlags.BIDIRECTIONAL + ); - notify["can-go-forward"].connect (() => { - redo_button.sensitive = can_go_forward; - }); + controller.notify["game-state"].connect (on_game_state_changed); notify["current-cell"].connect (() => { highlight_labels (previous_cell, false); highlight_labels (current_cell, true); + if (current_cell != null && + drawing_with_state != CellState.INVALID) { - if (drawing_with_state != CellState.UNDEFINED) { make_move_at_cell (); } }); - notify["strikeout-complete"].connect (() => { - update_all_labels_completeness (); - }); - - controller.notify["dimensions"].connect (() => { - // Update cell-size if required to fit on screen but without changing window size unnecessarily - // The dimensions may have increased or decreased so may need to increase or decrease cell size - // It is assumed up to 90% of the screen area can be used - var monitor_area = Gdk.Rectangle () { - width = 1024, - height = 768 - }; - - Gdk.Window? window = get_window (); - if (window != null) { - monitor_area = Utils.get_monitor_area (screen, window); - } - - var available_screen_width = monitor_area.width * 0.9 - 2 * GRID_BORDER - GRID_COLUMN_SPACING; - var max_cell_width = available_screen_width / (controller.dimensions.width * (1.0 + GRID_LABELBOX_RATIO)); - - var available_grid_height = (int)(window.get_height () - header_bar.get_allocated_height () - 2 * GRID_BORDER); - var opt_cell_height = (int)(available_grid_height / (controller.dimensions.height * (1.0 + GRID_LABELBOX_RATIO))); - - var available_screen_height = monitor_area.height * 0.9 - header_bar.get_allocated_height () - 2 * GRID_BORDER; - var max_cell_height = available_screen_height / (controller.dimensions.height * (1.0 + GRID_LABELBOX_RATIO)); - - var max_cell_size = (int)(double.min (max_cell_width, max_cell_height)); - if (max_cell_size < cell_size) { - cell_size = max_cell_size; - } else if (cell_size < opt_cell_height) { - cell_size = int.min (max_cell_size, opt_cell_height); - } - - }); - - cell_grid.leave_notify_event.connect (() => { + cell_grid.leave.connect (() => { row_clue_box.unhighlight_all (); column_clue_box.unhighlight_all (); - return false; - }); - - cell_grid.button_press_event.connect ((event) => { - if (event.type == Gdk.EventType.@2BUTTON_PRESS || event.button == Gdk.BUTTON_MIDDLE) { - drawing_with_state = controller.game_state == GameState.SOLVING ? CellState.UNKNOWN : CellState.EMPTY; - } else { - drawing_with_state = event.button == Gdk.BUTTON_PRIMARY ? CellState.FILLED : CellState.EMPTY; - } - - make_move_at_cell (); - return true; }); - key_release_event.connect ((event) => { - if (event.keyval == drawing_with_key) { - stop_painting (); - } - - return false; + update_style (); + settings.changed["follow-system-style"].connect (() => { + update_style (); }); - - cell_grid.button_release_event.connect (stop_painting); - // Force window to follow grid size in both native and flatpak installs - cell_grid.size_allocate.connect ((alloc) => { - Idle.add (() => { - var width = alloc.width * (1 + GRID_LABELBOX_RATIO); - var height = alloc.height * (1 + GRID_LABELBOX_RATIO); - resize ((int)width, (int)height); - return Source.REMOVE; - }); + settings.changed["prefer-dark-style"].connect (() => { + update_style (); }); + } - show_all (); + private void on_game_state_changed () { + var gs = controller.game_state; + update_all_labels_completeness (); + // restart_destructive = !model.is_blank (gs); + headerbar_manager.on_game_state_changed (gs); } + public string[] get_clues (bool is_column) { var label_box = is_column ? column_clue_box : row_clue_box; - return label_box.get_clues (); + return label_box.get_clue_texts (); } - public void update_labels_from_string_array (string[] clues, bool is_column) { + public void update_clues_from_string_array (string[] clues, bool is_column) { var clue_box = is_column ? column_clue_box : row_clue_box; - var lim = is_column ? controller.dimensions.width : controller.dimensions.height; + var lim = is_column ? controller.rows : controller.columns; for (int i = 0; i < lim; i++) { - clue_box.update_label_text (i, clues[i]); + clue_box.update_clue_text (i, clues[i]); } } - public void update_labels_from_solution () { - for (int r = 0; r < controller.dimensions.height; r++) { - row_clue_box.update_label_text (r, model.get_label_text_from_solution (r, false)); + public void update_clues_from_solution () { + for (int r = 0; r < controller.rows; r++) { + row_clue_box.update_clue_text ( + r, + model.get_label_text_from_solution (r, false) + ); } - for (int c = 0; c < controller.dimensions.width; c++) { - column_clue_box.update_label_text (c, model.get_label_text_from_solution (c, true)); + for (int c = 0; c < controller.columns; c++) { + column_clue_box.update_clue_text ( + c, + model.get_label_text_from_solution (c, true) + ); } update_all_labels_completeness (); } - public void make_move (Move m) { - if (!m.is_null ()) { - update_current_and_model (m.cell.state, m.cell); - } + public void make_move (Move m) requires (m.is_valid ()) { + update_current_and_model (m.cell.state, m.cell); } public void send_notification (string text) { - toast.title = text.dup (); - toast.send_notification (); + toast_overlay.add_toast (new Adw.Toast (text)); } public void show_working (Cancellable cancellable, string text = "") { cell_grid.frozen = true; // Do not show model updates - progress_indicator.text = text; schedule_show_progress (cancellable); + headerbar_manager.show_working (text); } public void end_working () { cell_grid.frozen = false; // Show model updates again - if (progress_timeout_id > 0) { Source.remove (progress_timeout_id); progress_timeout_id = 0; - } else { - progress_stack.set_visible_child_name ("Title"); } - update_all_labels_completeness (); - update_header_bar (); - } + headerbar_manager.hide_progress (); - private void update_header_bar () { - mode_switch.active = controller.game_state != GameState.SETTING; - - switch (controller.game_state) { - case GameState.SETTING: - set_buttons_sensitive (true); - break; - case GameState.SOLVING: - set_buttons_sensitive (true); - - break; - case GameState.GENERATING: - set_buttons_sensitive (false); - - break; - default: - break; - } + update_all_labels_completeness (); } - public void update_title () { - title_label.label = game_name; - title_label.tooltip_text = controller.current_game_path; - grade_label.label = game_grade.to_string (); + public void on_can_go_changed (bool forward, bool back) { + headerbar_manager.on_can_go_changed (forward, back); } - private void set_buttons_sensitive (bool sensitive) { - generate_button.sensitive = controller.game_state != GameState.GENERATING; - mode_switch.sensitive = sensitive; - load_game_button.sensitive = sensitive; - save_game_button.sensitive = sensitive; - save_game_as_button.sensitive = sensitive; - restart_destructive = sensitive && !model.is_blank (controller.game_state); - undo_button.sensitive = sensitive && can_go_back; - redo_button.sensitive = sensitive && can_go_forward; - check_correct_button.sensitive = sensitive && controller.game_state == GameState.SOLVING && can_go_back; - hint_button.sensitive = sensitive && controller.game_state == GameState.SOLVING; - auto_solve_button.sensitive = sensitive; - } + private void highlight_labels (Cell? c, bool is_highlight) { + if (c == null) { + return; + } - private void highlight_labels (Cell c, bool is_highlight) { - /* If c is NULL_CELL then will unhighlight all labels */ row_clue_box.highlight (c.row, is_highlight); column_clue_box.highlight (c.col, is_highlight); } private void update_all_labels_completeness () { - for (int r = 0; r < controller.dimensions.height; r++) { - update_label_complete (r, false); + for (int r = 0; r < controller.rows; r++) { + update_clue_complete (r, false); } - for (int c = 0; c < controller.dimensions.width; c++) { - update_label_complete (c, true); + for (int c = 0; c < controller.columns; c++) { + update_clue_complete (c, true); } } - private void update_label_complete (uint idx, bool is_col) { + private void update_clue_complete (uint idx, bool is_col) { var lbox = is_col ? column_clue_box : row_clue_box; - if (controller.game_state == GameState.SOLVING && strikeout_complete) { + if (controller.game_state == GameState.SOLVING) { var blocks = Gee.List.empty (); blocks = model.get_complete_blocks_from_working (idx, is_col); - lbox.update_label_complete (idx, blocks); + lbox.update_clue_complete (idx, blocks); } else { lbox.clear_formatting (idx); } } - private void make_move_at_cell (CellState state = drawing_with_state, Cell target = current_cell) { - if (target == NULL_CELL) { - return; - } - + private void make_move_at_cell ( + CellState state = drawing_with_state, + Cell? target = current_cell + ) requires (target != null) { var prev_state = model.get_data_for_cell (target); var cell = update_current_and_model (state, target); @@ -857,11 +378,17 @@ warning ("WITH DEBUGGING"); var col = current_cell.col; if (controller.game_state == GameState.SETTING) { - row_clue_box.update_label_text (row, model.get_label_text_from_solution (row, false)); - column_clue_box.update_label_text (col, model.get_label_text_from_solution (col, true)); + row_clue_box.update_clue_text ( + row, + model.get_label_text_from_solution (row, false) + ); + column_clue_box.update_clue_text ( + col, + model.get_label_text_from_solution (col, true) + ); } else { - update_label_complete (row, false); - update_label_complete (col, true); + update_clue_complete (row, false); + update_clue_complete (col, true); } return cell; @@ -874,51 +401,31 @@ warning ("WITH DEBUGGING"); private uint progress_timeout_id = 0; private void schedule_show_progress (Cancellable cancellable) { - progress_timeout_id = Timeout.add_full (Priority.HIGH_IDLE, PROGRESS_DELAY_MSEC, () => { - progress_indicator.cancellable = cancellable; - progress_stack.set_visible_child_name ("Progress"); - progress_timeout_id = 0; - return false; - }); + progress_timeout_id = Timeout.add_full ( + Priority.HIGH_IDLE, + PROGRESS_DELAY_MSEC, + () => { + headerbar_manager.show_progress (cancellable); + progress_timeout_id = 0; + return false; + } + ); } - private bool stop_painting () { - drawing_with_state = CellState.UNDEFINED; + private void stop_painting () { + drawing_with_state = CellState.INVALID; drawing_with_key = 0; - return false; - } - - /** With Control pressed, zoom using the fontsize. **/ - private bool on_grid_scroll_event (Gdk.EventScroll event) { - if (Gdk.ModifierType.CONTROL_MASK in event.state) { - switch (event.direction) { - case Gdk.ScrollDirection.UP: - change_cell_size (false); - break; - - case Gdk.ScrollDirection.DOWN: - change_cell_size (true); - break; - - default: - break; - } - - return true; - } - - return false; } /** Action callbacks **/ private void action_restart () { controller.restart (); - if (controller.game_state == GameState.SETTING) { - game_grade = Difficulty.UNDEFINED; - } + // if (controller.game_state == GameState.SETTING) { + // game_grade = Difficulty.UNDEFINED; + // } } - private void action_solve () { + private void action_computer_solve () requires (controller.game_state == GameState.SETTING) { controller.computer_solve (); } @@ -927,7 +434,20 @@ warning ("WITH DEBUGGING"); } private void action_options () { - app_menu.activate (); + menu_button.activate (); + } + + private void action_preferences () { + headerbar_manager.popdown_menus (); + var dialog = new PreferencesDialog () { + transient_for = this, + title = _("Preferences") + }; + dialog.response.connect (() => { + // Changes mediated by settings schema + dialog.destroy (); + }); + dialog.present (); } #if WITH_DEBUGGING @@ -953,27 +473,52 @@ warning ("WITH DEBUGGING"); } private void action_save () { - controller.save_game (); + controller.save_game.begin (); } private void action_save_as () { - controller.save_game_as (); + controller.save_game_as.begin (); } - private void action_zoom_in () { - change_cell_size (true); + private void action_zoom_larger () { + var current_width = this.default_width; + this.default_width = current_width + current_width / 10; + var current_height = this.default_height; + this.default_height = current_height + current_height / 10; } - private void action_zoom_out () { - change_cell_size (false); + + private void action_zoom_smaller () { + var current_width = this.default_width; + this.default_width = current_width - current_width / 10; + var current_height = this.default_height; + this.default_height = current_height - current_height / 10; } - private void change_cell_size (bool increase) { - var delta = double.max (ZOOM_RATIO * cell_size, 1.0); - if (increase) { - cell_size += (int)delta; - } else { - cell_size -= (int)delta; - } + private void action_zoom_default () { + this.default_width = DEFAULT_WIDTH; + this.default_height = DEFAULT_HEIGHT; + } + + private void action_shortcut_window () { + var helper = new ShortcutHelper (); + helper.show_window (); + } + + private void action_about_dialog () { + Gtk.show_about_dialog ( + this, + "authors", new string[1] {"Jeremy Wootten"}, + "comments", _("An implementation of the Japanese logic puzzle \"Nonograms\" written in Vala, allowing the user to solve computer generated puzzles or design their own."), + "copyright", _("2010-2024 Jeremy Wootten"), + "license_type", Gtk.License.LGPL_2_1, + "logo_icon_name", "com.github.jeremypw.gnonograms", + "program_name", _("Gnonograms"), + "translator_credits", "NathanBnm (French)\n André Barata (Portuguese)\n Heimen Stoffels (Dutch)", + "version", "4.0.0", + "website", "https://github.com/jeremypw/gnonograms", + "website_label", "Source Code", + null + ); } private void action_check_errors () { @@ -995,16 +540,20 @@ warning ("WITH DEBUGGING"); move_cursor (0, 1); } private void move_cursor (int row_delta, int col_delta) { - if (current_cell == NULL_CELL) { + if (current_cell == null) { + update_current_cell ({ 0, 0, CellState.INVALID }); return; } - Cell target = {current_cell.row + row_delta, - current_cell.col + col_delta, - CellState.UNDEFINED - }; + var target = Cell () { + row = current_cell.row + row_delta, + col = current_cell.col + col_delta, + state = CellState.INVALID + }; + + if (target.row >= controller.rows || + target.col >= controller.columns) { - if (target.row >= controller.dimensions.height || target.col >= controller.dimensions.width) { return; } @@ -1012,23 +561,27 @@ warning ("WITH DEBUGGING"); } private void action_setting_mode () { - controller.game_state = GameState.SETTING; + controller.change_mode (SETTING); } private void action_solving_mode () { - controller.game_state = GameState.SOLVING; + controller.change_mode (SOLVING); } private void action_generating_mode () { - controller.game_state = GameState.GENERATING; + controller.change_mode (GENERATING); } - private void action_paint_filled () { + private void paint_filled () { paint_cell_state (CellState.FILLED); + drawing_with_key = paint_fill_key; } - private void action_paint_empty () { + private void paint_empty () { paint_cell_state (CellState.EMPTY); + drawing_with_key = paint_empty_key; } - private void action_paint_unknown () { + + private void paint_unknown () { paint_cell_state (CellState.UNKNOWN); + drawing_with_key = paint_unknown_key; } private void paint_cell_state (CellState cs) { if (cs == CellState.UNKNOWN && controller.game_state != GameState.SOLVING) { @@ -1036,11 +589,44 @@ warning ("WITH DEBUGGING"); } drawing_with_state = cs; - var current_event = Gtk.get_current_event (); - if (current_event.type == Gdk.EventType.KEY_PRESS) { - drawing_with_key = ((Gdk.EventKey)current_event).keyval; - } make_move_at_cell (); } + + // Code based largely on elementary Code app + private ulong color_scheme_listener_handler_id = 0; + private void update_style () { + var gtk_settings = Gtk.Settings.get_default (); + var granite_settings = Granite.Settings.get_default (); + var following_system = settings.get_boolean ("follow-system-style"); + disconnect_color_scheme_preference_listener (following_system); + if (following_system) { + gtk_settings.gtk_application_prefer_dark_theme = ( + granite_settings.prefers_color_scheme == DARK + ); + color_scheme_listener_handler_id = granite_settings.notify["prefers-color-scheme"].connect (() => { + gtk_settings.gtk_application_prefer_dark_theme = ( + granite_settings.prefers_color_scheme == DARK + ); + }); + } else { + gtk_settings.gtk_application_prefer_dark_theme = settings.get_boolean ("prefer-dark-style"); + color_scheme_listener_handler_id = settings.notify["prefers-dark-style"].connect (() => { + gtk_settings.gtk_application_prefer_dark_theme = settings.get_boolean ("prefer-dark-style"); + }); + } + } + + private void disconnect_color_scheme_preference_listener (bool following_system) { + if (color_scheme_listener_handler_id != 0) { + if (following_system) { + var granite_settings = Granite.Settings.get_default (); + granite_settings.disconnect (color_scheme_listener_handler_id); + } else { + settings.disconnect (color_scheme_listener_handler_id); + } + + color_scheme_listener_handler_id = 0; + } + } } diff --git a/src/dialogs/PreferencesDialog.vala b/src/dialogs/PreferencesDialog.vala new file mode 100644 index 0000000..bb2cd4e --- /dev/null +++ b/src/dialogs/PreferencesDialog.vala @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + +public class Gnonograms.PreferencesDialog : Granite.Dialog { + construct { + set_default_size (400, 100); + resizable = false; + // //TODO Add Clue help switch + + var empty_color_dialog = new Gtk.ColorDialog () { + title = _("Filled Color"), + with_alpha = true + }; + var empty_color_button = new Gtk.ColorDialogButton (empty_color_dialog); + var empty_color = settings.get_string ("empty-color"); + var rgba = Gdk.RGBA (); + if (rgba.parse (empty_color)) { + empty_color_button.set_rgba (rgba); + } + + empty_color_button.notify["rgba"].connect (() => { + settings.set_string ("empty-color", empty_color_button.get_rgba ().to_string ()); + }); + var empty_color_preference = new PreferenceRow (_("Color of empty cells"), empty_color_button); + + var filled_color_dialog = new Gtk.ColorDialog () { + title = _("Filled Color"), + with_alpha = true + }; + var filled_color_button = new Gtk.ColorDialogButton (filled_color_dialog); + var filled_color = settings.get_string ("filled-color"); + if (rgba.parse (filled_color)) { + filled_color_button.set_rgba (rgba); + } + + filled_color_button.notify["rgba"].connect (() => { + warning ("filled color now %s", filled_color_button.get_rgba ().to_string ()); + settings.set_string ("filled-color", filled_color_button.get_rgba ().to_string ()); + }); + + var filled_color_preference = new PreferenceRow (_("Color of filled cells"), filled_color_button); + + var follow_system_switchmodelbutton = new Granite.SwitchModelButton (_("Follow System Style")) { + margin_top = 3 + }; + + var color_mode_switch = new Granite.ModeSwitch.from_icon_name ( + "weather-clear-symbolic", + "weather-clear-night-symbolic" + ) { + primary_icon_tooltip_text = _("Light"), + secondary_icon_tooltip_text = _("Dark") + }; + var color_mode_preference = new PreferenceRow (_("Color Style"), color_mode_switch) { + margin_start = margin_start + 12 + }; + var color_revealer = new Gtk.Revealer (); + color_revealer.set_child (color_mode_preference); + follow_system_switchmodelbutton.bind_property ( + "active", + color_revealer, "reveal-child", + INVERT_BOOLEAN | SYNC_CREATE + ); + + var main_box = new Gtk.Box (VERTICAL, 12) { + margin_start = 12 + }; + + // main_box.append (grade_preference); + // main_box.append (row_preference); + // main_box.append (column_preference); + main_box.append (filled_color_preference); + main_box.append (empty_color_preference); + main_box.append (follow_system_switchmodelbutton); + main_box.append (color_revealer); + + get_content_area ().append (main_box); + add_button (_("Close"), Gtk.ResponseType.APPLY); + + // settings.bind ("columns", column_setting, "value", DEFAULT); + // settings.bind ("rows", row_setting, "value", DEFAULT); + + // grade_setting.selected = settings.get_enum ("grade"); + // grade_setting.notify["selected"].connect (() => { + // settings.set_enum ("grade", (Difficulty)(grade_setting.selected)); + // }); + + settings.bind ( + "follow-system-style", + follow_system_switchmodelbutton, + "active", + SettingsBindFlags.DEFAULT + ); + + settings.bind ( + "prefer-dark-style", + color_mode_switch, + "active", + SettingsBindFlags.DEFAULT + ); + } +} diff --git a/libcore/Constants.vala b/src/misc/Constants.vala similarity index 51% rename from libcore/Constants.vala rename to src/misc/Constants.vala index 620ab42..576c488 100644 --- a/libcore/Constants.vala +++ b/src/misc/Constants.vala @@ -1,35 +1,20 @@ - -/* Constants.vala - * Copyright (C) 2010-2021 Jeremy Wootten - * - 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 . +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ - namespace Gnonograms { - public const Cell NULL_CELL = { uint.MAX, uint.MAX, CellState.UNDEFINED }; public const uint MAXSIZE = 54; // max number rows or columns public const uint MINSIZE = 5; // Change to 1 when debugging public const uint SIZESTEP = 5; // Change to 1 when debugging - public const double GRID_LABELBOX_RATIO = 0.3; // For simplicity give labelboxes fixed ratio of cellgrid dimension + public const Difficulty MIN_GRADE = Difficulty.EASY; /* TRIVIAL and VERY EASY GRADES not worth supporting */ public const string BLOCKSEPARATOR = ", "; public const string BLANKLABELTEXT = N_("?"); public const string GAMEFILEEXTENSION = ".gno"; public const string UNSAVED_FILENAME = "Unsaved Game" + GAMEFILEEXTENSION; - public const string UNTITLED_NAME = N_("Untitled"); + public const string APP_NAME = "Gnonograms"; public const string SETTING_FILLED_COLOR = "#000000"; /* Elementary Black 900 */ public const string SETTING_EMPTY_COLOR = "#fafafa"; /* Elementary Silver 100 */ diff --git a/src/misc/Enums.vala b/src/misc/Enums.vala new file mode 100644 index 0000000..2f1195d --- /dev/null +++ b/src/misc/Enums.vala @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +namespace Gnonograms { + public enum Difficulty { + EASY = 0, + MODERATE = 1, + HARD = 2 , + CHALLENGING = 3, + ADVANCED = 4, + MAXIMUM = 5, /* Max grade for generated puzzles (possibly ambiguous)*/ + COMPUTER = 9, /* Grade for requested computer solving */ + UNDEFINED = 99; + + public string to_string () { + switch (this) { + case Difficulty.EASY: + return _("Easy"); + case Difficulty.MODERATE: + return _("Moderately difficult"); + case Difficulty.HARD: + return _("Difficult"); + case Difficulty.CHALLENGING: + return _("Very Difficult"); + case Difficulty.ADVANCED: + return _("Advanced logic required"); + case Difficulty.MAXIMUM: + return _("Possibly ambiguous"); + case Difficulty.COMPUTER: + return _("Super human"); + case Difficulty.UNDEFINED: + return ""; + default: + critical ("grade to string - unexpected grade"); + assert_not_reached (); + } + } + + public static string[] all_human () { + return { + EASY.to_string (), + MODERATE.to_string (), + HARD.to_string (), + CHALLENGING.to_string (), + ADVANCED.to_string (), + MAXIMUM.to_string () + }; + } + } + + public enum SolverState { + ERROR = 0, + CANCELLED = 1, + NO_SOLUTION = 1 << 1, + SIMPLE = 1 << 2, + ADVANCED = 1 << 3, + AMBIGUOUS = 1 << 4, + UNDEFINED = 1 << 5; + + public bool solved () { + return this == SIMPLE || this == ADVANCED || this == AMBIGUOUS; + } + } + + public enum CellPatternType { + CELL, + HIGHLIGHT, + UNDEFINED + } + + public enum GamePatternType { + SIMPLE_RANDOM, + UNDEFINED + } +} diff --git a/src/misc/Structs.vala b/src/misc/Structs.vala new file mode 100644 index 0000000..f782d3e --- /dev/null +++ b/src/misc/Structs.vala @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.Block { + public int length; + public bool is_complete; + public bool is_error; + + public Block (int len, bool complete = false, bool error = false) { + length = len; + is_complete= complete; + is_error = error; + } + + public Block.null () { + length = -1; + is_complete = false; + is_error = false; + } + + public bool is_null () { + return length < 0; + } +} + +// public struct Gnonograms.Cell { +// public uint row; +// public uint col; +// public CellState state; + +// public bool same_coords (Cell c) { +// return (this.row == c.row && this.col == c.col); +// } + +// public bool equal (Cell b) { +// return ( +// this.row == b.row && +// this.col == b.col && +// this.state == b.state +// ); + +// } + +// public Cell inverse () { +// Cell c = {row, col, CellState.UNKNOWN }; + +// if (this.state == CellState.EMPTY) { +// c.state = CellState.FILLED; +// } else { +// c.state = CellState.EMPTY; +// } + +// return c; +// } + +// public Cell clone () { +// return { row, col, state }; +// } + +// public string to_string () { +// return "Row %u, Col %u, State %s".printf (row, col, state.to_string ()); +// } +// } + +public struct Gnonograms.Dimensions { + uint width; + uint height; + + public uint area () { + return width * height; + } + + public uint length () { + return width + height; + } + + public bool equal (Dimensions other) { + return width == other.width && height == other.height; + } +} + +public struct Gnonograms.FilterInfo { + string name; + string[] patterns; +} + +public struct Gnonograms.Range { //can use for filled subregions or ranges of filled and unknown cells + public int start; + public int end; + public int filled; + public int unknown; + + public int length () { + return end - start + 1; + } +} diff --git a/libcore/utils.vala b/src/misc/utils.vala similarity index 68% rename from libcore/utils.vala rename to src/misc/utils.vala index 18e45a7..67e8fc7 100644 --- a/libcore/utils.vala +++ b/src/misc/utils.vala @@ -1,20 +1,8 @@ -/* utils.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ namespace Gnonograms.Utils { public static string[] remove_blank_lines (string[] sa) { @@ -157,7 +145,7 @@ namespace Gnonograms.Utils { public string block_string_from_cellstate_array (CellState[] cellstates) { StringBuilder sb = new StringBuilder (""); - CellState count_state = CellState.UNDEFINED; + CellState count_state = CellState.INVALID; int count = 0, blocks = 0; bool counting = false; foreach (var state in cellstates) { @@ -168,16 +156,15 @@ namespace Gnonograms.Utils { blocks++; } else if (count_state == CellState.UNKNOWN) { sb.append ("?" + BLOCKSEPARATOR); - } counting = false; - count_state = CellState.UNDEFINED; + count_state = CellState.INVALID; count = 0; break; case CellState.FILLED: - if (count_state == CellState.UNDEFINED) { + if (count_state == CellState.INVALID) { count = 0; counting = true; } else if (count_state == CellState.UNKNOWN) { @@ -190,7 +177,7 @@ namespace Gnonograms.Utils { break; case CellState.UNKNOWN: - if (count_state == CellState.UNDEFINED) { + if (count_state == CellState.INVALID) { counting = true; } else if (count_state == CellState.FILLED) { sb.append (count.to_string () + BLOCKSEPARATOR); @@ -211,7 +198,9 @@ namespace Gnonograms.Utils { } else if (count_state == CellState.UNKNOWN) { sb.append ("?" + BLOCKSEPARATOR); blocks++; - } if (blocks == 0) { + } + + if (blocks == 0) { sb.append ("0"); } else { sb.truncate (sb.len - BLOCKSEPARATOR.length); // remove trailing seperator @@ -224,7 +213,7 @@ namespace Gnonograms.Utils { CellState[] cs = {}; string[] blocks = remove_blank_lines (s.split_set (BLOCKSEPARATOR)); foreach (var block in blocks) { - cs += (CellState)(int.parse (block)).clamp (0, CellState.UNDEFINED); + cs += (CellState)(int.parse (block)).clamp (0, CellState.INVALID); } return cs; @@ -240,11 +229,12 @@ namespace Gnonograms.Utils { return sb.str; } - public static int show_dlg (string primary_text, - Gtk.MessageType type, - string? secondary_text, - Gtk.Window? parent) { - + private static int show_dlg ( + string primary_text, + Gtk.MessageType type, + string? secondary_text, + Gtk.Window? parent + ) { string icon_name = ""; var buttons = Gtk.ButtonsType.CLOSE; switch (type) { @@ -269,9 +259,11 @@ namespace Gnonograms.Utils { assert_not_reached (); } - var dialog = new Granite.MessageDialog.with_image_from_icon_name (primary_text, - secondary_text ?? "", - icon_name, buttons); + var dialog = new Granite.MessageDialog.with_image_from_icon_name ( + primary_text, + secondary_text ?? "", + icon_name, buttons + ); dialog.set_transient_for (parent); if (type == Gtk.MessageType.QUESTION) { @@ -280,23 +272,29 @@ namespace Gnonograms.Utils { dialog.set_default_response (Gtk.ResponseType.NO); } - dialog.set_position (Gtk.WindowPosition.MOUSE); - int response = dialog.run (); - dialog.destroy (); + Gtk.ResponseType response = Gtk.ResponseType.NO; + dialog.response.connect ((resp) => { + dialog.destroy (); + response = (Gtk.ResponseType)resp; + }); + + dialog.show (); return response; } - public static void show_error_dialog (string primary_text, - string? secondary_text = null, - Gtk.Window? parent = null) { - + public static void show_error_dialog ( + string primary_text, + string? secondary_text = null, + Gtk.Window? parent = null + ) { show_dlg (primary_text, Gtk.MessageType.ERROR, secondary_text, parent); } - public static bool show_confirm_dialog (string primary_text, - string? secondary_text = null, - Gtk.Window? parent = null) { - + public static bool show_confirm_dialog ( + string primary_text, + string? secondary_text = null, + Gtk.Window? parent = null + ) { var response = show_dlg ( primary_text, Gtk.MessageType.QUESTION, @@ -306,53 +304,32 @@ namespace Gnonograms.Utils { return response == Gtk.ResponseType.YES; } - public static string? get_open_save_path (Gtk.Window? parent, - string dialogname, - bool save, - string start_path, - string basename) { - string? file_path = null; - string button_label = save ? _("Save") : _("Open"); - var gtk_action = save ? Gtk.FileChooserAction.SAVE : Gtk.FileChooserAction.OPEN; - var dialog = new Gtk.FileChooserNative ( - dialogname, - parent, - gtk_action, - button_label, - _("Cancel") - ); + public static async File? get_open_save_file ( + Gtk.Window? parent, + string dialogname, + bool save, + string start_folder_path, + string basename + ) throws Error { + var button_label = save ? _("Save") : _("Open"); + var dialog = new Gtk.FileDialog () { + title = dialogname, + accept_label = button_label, + initial_folder = File.new_for_path (start_folder_path), + initial_name = basename, + modal = true + + }; dialog.set_modal (true); - try { - if (save) { - dialog.set_current_folder_file (File.new_for_path (start_path)); - if (basename != null) { - dialog.set_current_name (basename); - } - } else { - try { - dialog.set_current_folder_file (File.new_for_path (start_path)); - } catch (Error e) { - warning ("Error setting current folder: %s", e.message); - } - } - } catch (Error e) { - warning ("Error configuring FileChooser dialog: %s", e.message); - } - - var response = dialog.run (); - if (response == Gtk.ResponseType.ACCEPT) { - file_path = dialog.get_filename (); + File? result = null; + warning ("show dialog"); + if (save) { + result = yield (dialog.save (parent, null)); + } else { + result = yield (dialog.open (parent, null)); } - - dialog.destroy (); - - return file_path; - } - - public Gdk.Rectangle get_monitor_area (Gdk.Screen screen, Gdk.Window window) { - var display = Gdk.Display.get_default (); - var monitor = display.get_monitor_at_window (window); - return monitor.get_geometry (); + warning ("done"); + return result; } } diff --git a/src/objects/Move.vala b/src/objects/Move.vala new file mode 100644 index 0000000..91ab808 --- /dev/null +++ b/src/objects/Move.vala @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.Move { + public Cell cell; + public CellState previous_state; + + public Move.from_cell (Cell _cell, CellState _previous_state) { + cell = Cell () { + row =_cell.row, + col =_cell.col, + state = _cell.state + }; + + previous_state = _previous_state; + } + + public Move (uint _row, uint _col, CellState _state, CellState _previous_state) { + cell = Cell () { + row =_row, + col =_col, + state = _state + }; + + previous_state = _previous_state; + } + + public bool is_valid () { + return ( + cell.row < MAXSIZE && + cell.col < MAXSIZE && + cell.state < CellState.COMPLETED && + previous_state < CellState.COMPLETED + ); + } + + public bool equal (Move? m) { + return m != null && (m.cell.equal (cell) && m.previous_state == previous_state); + } + + public Move clone () { + return new Move.from_cell (this.cell.clone (), this.previous_state); + } + + public string to_string () { + return "%u,%u,%u,%u".printf (cell.row, cell.col, cell.state, previous_state); + } + + public static Move? from_string (string s) throws ConvertError { + var parts = s.split (","); + if (parts == null || parts.length != 4) { + // return Move.null_move; + throw new ConvertError.FAILED ("Incorrect number of parts"); + } + + var row = (uint)(int.parse (parts[0])); + var col = (uint)(int.parse (parts[1])); + var state = (uint)(int.parse (parts[2])); + var previous_state = (uint)(int.parse (parts[3])); + var mv = new Move (row, col, state, previous_state); + if (mv.is_valid ()) { + return mv; + } else { + throw new ConvertError.FAILED ("Invalid parameters"); + } + } +} diff --git a/libcore/My2DCellArray.vala b/src/objects/My2DCellArray.vala similarity index 84% rename from libcore/My2DCellArray.vala rename to src/objects/My2DCellArray.vala index a7ef396..f087074 100644 --- a/libcore/My2DCellArray.vala +++ b/src/objects/My2DCellArray.vala @@ -1,22 +1,9 @@ -/* My2DCellArray.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ - public class Gnonograms.My2DCellArray : Object { public uint rows { get; construct; } public uint cols { get; construct; } diff --git a/libcore/Region.vala b/src/objects/Region.vala similarity index 91% rename from libcore/Region.vala rename to src/objects/Region.vala index 890442a..e9b962c 100644 --- a/libcore/Region.vala +++ b/src/objects/Region.vala @@ -1,22 +1,9 @@ -/* Region.vala - * Copyright (C) 2010 -2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 < http://www.gnu.org/licenses/>. - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ - /* A region consists of a one dimensional array of cells, corresponding to a row or column of the puzzle. Associated with this are: @@ -100,7 +87,6 @@ public class Gnonograms.Region { this.grid = grid; uint max_len = uint.max (grid.rows, grid.cols); uint max_blocks = max_len / 2 + 2; - status = new CellState[max_len]; status_backup = new CellState[max_len]; ranges = new int[max_blocks, 4]; @@ -118,11 +104,9 @@ public class Gnonograms.Region { this.is_column = is_column; this.n_cells = (int)n_cells; this.clue = clue; - temp_status = new CellState[n_cells]; temp_status2 = new CellState[n_cells]; int[] clue_blocks = Utils.block_array_from_clue (clue); - n_blocks = clue_blocks.length; can_be_empty_pointer = n_blocks; //flag for cell that may be empty is_finished_pointer = n_blocks + 1; //flag for finished cell (filled or empty?) @@ -217,10 +201,8 @@ public class Gnonograms.Region { * */ message = ""; in_error = false; - // Get external changes get_status (); - //Is complete or has a (invalid) change been made by another region if (in_error) { return false; @@ -494,7 +476,12 @@ public class Gnonograms.Region { while (current_index < n_cells) { //find a filled sub -region start_is_capped = false; end_is_capped = false; - current_index = seek_next_required_status (CellState.FILLED, current_index, n_cells, 1); + current_index = seek_next_required_status ( + CellState.FILLED, + current_index, + n_cells, + 1 + ); if (current_index == n_cells) { break; @@ -510,9 +497,14 @@ public class Gnonograms.Region { start_is_capped = true; //edge cell } - length = count_consecutive_with_state_from (CellState.FILLED, current_index, true); //current_index not changed - int lastcell = current_index + length - 1; //last filled cell in this (partial) block + length = count_consecutive_with_state_from ( + CellState.FILLED, + current_index, + true //current_index not changed + ); + // @lastcell: last filled cell in this (partial) block + int lastcell = current_index + length - 1; if (lastcell == n_cells - 1 || status[lastcell + 1] == CellState.EMPTY) { end_is_capped = true; //last cell is at edge } @@ -524,8 +516,8 @@ public class Gnonograms.Region { continue; } else { //find largest possible owner of this (partial) block int largest = find_largest_possible_block_for_cell (current_index); - - if (largest == length) {//there is **at least one** largest block that fits exactly. + //Test if there is **at least one** largest block that fits exactly + if (largest == length) { // this region must therefore be complete assign_and_cap_range (current_index, length); current_index += length + 1; @@ -601,7 +593,8 @@ public class Gnonograms.Region { } } - current_index += length; //move past block - if reaches here no operations have been performed on block + //move past block - no operations have been performed on block + current_index += length; } return changed; @@ -621,7 +614,8 @@ public class Gnonograms.Region { continue; //is following cell empty? } - if (get_sole_owner (idx) < 0) { // if owner ambiguous, can only deal with single cell gap + // if owner ambiguous, can only deal with single cell gap + if (get_sole_owner (idx) < 0) { // see if single cell gap which can be marked empty because // to fill it would create a block larger than any permissible. if (status[idx + 2] != CellState.FILLED) { @@ -629,15 +623,21 @@ public class Gnonograms.Region { } // we have found a one cell gap // calculate total length if gap were to be FILLED. - int block_length = count_consecutive_with_state_from (CellState.FILLED, idx + 2, true) + - count_consecutive_with_state_from (CellState.FILLED, idx, false) + 1; + int block_length = ( + count_consecutive_with_state_from ( + CellState.FILLED, idx + 2, true + ) + + count_consecutive_with_state_from ( + CellState.FILLED, idx, false + ) + 1 + ); bool must_be_empty = true; //look for a possible owner at least as long as combined length for (int bl = 0; bl < n_blocks; bl++) { - - if (tags[idx, bl] && blocks[bl] >= block_length) { //possible owner found - gap could be filled + //If possible owner found - gap could be filled + if (tags[idx, bl] && blocks[bl] >= block_length) { must_be_empty = false; break; } @@ -692,13 +692,11 @@ public class Gnonograms.Region { total_count = count_to_the_left + count_to_the_right; if (total_count == 2) { - for (int i = 0; i < n_blocks; i++) { - if (tags[ptr, i] && blocks[i] <= length_to_the_right) { - if (tags[idx, i] && blocks[i] <= length_to_the_left) { - must_not_be_empty = true; // only one block fits in both sides + // only one block fits in both sides + must_not_be_empty = true; } } } @@ -713,7 +711,8 @@ public class Gnonograms.Region { } idx += 2; //skip gap - } else { //only one possible owner of first FILLED cell + } else { + //only one possible owner of first FILLED cell int cell1 = idx; //start of gap idx++; @@ -727,7 +726,9 @@ public class Gnonograms.Region { } else { //if start and end of gap have same owner, fill in the gap. int owner = have_same_owner (cell1, idx); if (owner >= 0) { - changed = set_range_owner (owner, cell1, idx - cell1 + 1, true, false) || changed; + changed = changed || set_range_owner ( + owner, cell1, idx - cell1 + 1, true, false + ); } idx--; @@ -765,7 +766,8 @@ public class Gnonograms.Region { } int s = idx; //first cell with block i as possible owner - int l = count_contiguous_with_same_owner_from (i, idx); //length of contiguous cells having this block (i) as a possible owner. + //length of contiguous cells having this block (i) as a possible owner. + int l = count_contiguous_with_same_owner_from (i, idx); if (l < blocks[i]) { remove_block_from_range (i, s, l, 1); //block cannot be here @@ -801,7 +803,6 @@ public class Gnonograms.Region { int end = start + length - 1; for (int i = 0; i < n_blocks; i++) { - if (completed_blocks[i]) { continue; } @@ -845,7 +846,6 @@ public class Gnonograms.Region { //remove as possible owner blocks between first and last that are wrong length for (int i = first + 1; i < last; i++) { - if (blocks[i] == length) { continue; } @@ -977,12 +977,18 @@ public class Gnonograms.Region { //current_index points to cell on the edge if (status[current_index] == CellState.FILLED) { //first cell is FILLED. Can complete whole block - return set_block_complete_and_cap (current_block_number, current_index, direction); + return set_block_complete_and_cap ( + current_block_number, + current_index, + direction + ); } else { // see if filled cell in range of first block and complete after that int start_of_edge = current_index; int start_of_filling = -1; int block_length = blocks[current_block_number]; - int blocklimit = (dir? current_index + block_length : current_index - block_length); + int blocklimit = dir? + current_index + block_length : + current_index - block_length; if (blocklimit < -1 && blocklimit > n_cells) { in_error = true; @@ -990,7 +996,12 @@ public class Gnonograms.Region { return false; } - current_index = seek_next_required_status (CellState.FILLED, current_index, blocklimit, direction); + current_index = seek_next_required_status ( + CellState.FILLED, + current_index, + blocklimit, + direction + ); if (current_index != blocklimit) { start_of_filling = current_index; @@ -1014,7 +1025,9 @@ public class Gnonograms.Region { // an unfilled cell found. FILL cells beyond first FILLED cells. // remove block from out of range of first filled cell. - while (current_index != blocklimit && status[current_index] == CellState.FILLED) { + while (current_index != blocklimit && + status[current_index] == CellState.FILLED + ) { set_cell_owner (current_index, current_block_number, true, false); set_cell_empty (start_of_edge); changed = true; @@ -1034,7 +1047,11 @@ public class Gnonograms.Region { current_index = start_of_filling + (dir ? block_length : -block_length); if (current_index >= 0 && current_index < n_cells) { - remove_block_from_cell_to_end (current_block_number, current_index, direction); + remove_block_from_cell_to_end ( + current_block_number, + current_index, + direction + ); } } @@ -1050,7 +1067,6 @@ public class Gnonograms.Region { //starting point is set in current_index and current_block_number before calling. bool dir = (direction == FORWARDS); int loop_step = dir ? 1 : -1; - for (int i = current_index; (i >= 0 && i < n_cells); i += loop_step) { if (status[i] == CellState.EMPTY) { continue; @@ -1058,9 +1074,13 @@ public class Gnonograms.Region { //now pointing at first cell of filled or unknown block after edge if (tags[i, is_finished_pointer]) { //skip to end of finished block - i += (dir ? blocks[current_block_number] - 1 : 1 - blocks[current_block_number]); + i += (dir ? + blocks[current_block_number] - 1 : + 1 - blocks[current_block_number] + ); //now pointing at last cell of filled block - var next_block = current_block_number + loop_step; //Increment or decrement current block as appropriate + var next_block = current_block_number + loop_step; + //Increment or decrement current block as appropriate if (next_block >= 0 || next_block < n_blocks - 1) { current_block_number = next_block; } else { @@ -1079,20 +1099,21 @@ public class Gnonograms.Region { // blocks may have been marked completed - thereby reducing available ranges int[] available_blocks = get_blocks_available (); int bl = available_blocks.length; - if (bl == 0) { return false; } - //update ranges with currently available ranges (can contain only unknown and incomplete cells) + // update ranges with currently available ranges + // (can contain only unknown and incomplete cells) int n_available_ranges = count_available_ranges (false); if (n_available_ranges == 0) { return false; } - int[,] block_start = new int[bl, 2]; //range number and offset of earliest start point - int[,] block_end = new int[bl, 2]; //range number and offset of latest end point - + //range number and offset of earliest start point + int[,] block_start = new int[bl, 2]; + //range number and offset of latest end point + int[,] block_end = new int[bl, 2]; //find earliest start point of each block (treating ranges as all unknown cells) int rng = 0; int offset = 0; @@ -1102,11 +1123,9 @@ public class Gnonograms.Region { for (int b = 0; b < bl; b++) {//for each available block length = blocks[available_blocks[b]]; //get its length - if (ranges[rng, 1] < (length + offset)) {//cannot fit in current range rng++; offset = 0; //skip to start of next range - while (rng < n_available_ranges && ranges[rng, 1] < length) { rng++; //keep skipping if too small } @@ -1119,7 +1138,6 @@ public class Gnonograms.Region { //look for collision with filled cell ptr = ranges[rng, 0] + offset + length; //cell after end of block start = ptr; - while (ptr < n_cells && !tags[ptr, can_be_empty_pointer]) { ptr++; offset++; @@ -1133,14 +1151,11 @@ public class Gnonograms.Region { //carry out same process in reverse to get latest end points rng = n_available_ranges - 1; offset = 0; //start at end of last range NB offset now counts from end - for (int b = bl - 1; b >= 0; b--) { //start at last block length = blocks[available_blocks[b]]; //get length - if (ranges[rng, 1] < (length + offset)) { //doesn't fit rng --; offset = 0; - while (rng >= 0 && ranges[rng, 1] < length) { rng --; //keep skipping if too small } @@ -1151,9 +1166,9 @@ public class Gnonograms.Region { } //look for collision with filled cell - ptr = ranges[rng, 0] + ranges[rng, 1] - (offset + length) - 1; //cell before beginning of block + //cell before beginning of block + ptr = ranges[rng, 0] + ranges[rng, 1] - (offset + length) - 1; start = ptr; - while (ptr >= 0 && !tags[ptr, can_be_empty_pointer]) { ptr --; offset++; @@ -1194,7 +1209,8 @@ public class Gnonograms.Region { remove_block_from_range (available_blocks[b], start, length, 1); } - for (int r = n_available_ranges - 1; r > block_end[b, 0]; r--) { //ranges after possible + //ranges after possible + for (int r = n_available_ranges - 1; r > block_end[b, 0]; r--) { remove_block_from_range (available_blocks[b], ranges[r, 0], ranges[r, 1], 1); } } @@ -1220,16 +1236,12 @@ public class Gnonograms.Region { for (int rng = 0; rng < n_ranges; rng++) { start = ranges[rng, 0]; length = ranges[rng, 1]; - for (idx = start; idx < start + length; idx++) { int count = 0; int impossible = 0; - for (int b = 0; b < n_blocks; b++) { - if (tags[idx, b]) { count++; - if (blocks[b] != length) { tags[idx, b] = false; impossible++; @@ -1238,9 +1250,12 @@ public class Gnonograms.Region { } if (count == impossible) { - record_error ("capped range audit", - "start %i len %i, n_blocks %u count %i, impossible %i filled cell with no owners" - .printf (start, length, n_blocks, count, impossible)); + record_error ( + "capped range audit", + "start %i len %i, n_blocks %u count %i, impossible %i filled cell with no owners".printf ( + start, length, n_blocks, count, impossible + ) + ); return false; } } @@ -1250,14 +1265,15 @@ public class Gnonograms.Region { } private bool available_filled_subregion_audit () { - //test whether there is an unambiguous distribution of available blocks amongs available filled subregions. + //test whether there is an unambiguous distribution of available blocks + //amongst available filled subregions. int idx = 0; int start = 0; int end = n_cells; int region_count = 0; - Range[] available_subregions = new Range[n_cells / 2]; //start and end of each subregion - + //start and end of each subregion + Range[] available_subregions = new Range[n_cells / 2]; while (idx < n_cells) { if (status[idx] != CellState.FILLED) { idx++; @@ -1265,7 +1281,6 @@ public class Gnonograms.Region { } region_count++; - if (region_count <= n_blocks) { start = idx; } else { @@ -1292,7 +1307,6 @@ public class Gnonograms.Region { //now see how many blocks could fit here; int[] available_blocks = get_blocks_available (); int n_available_blocks = available_blocks.length; - if (region_count > n_available_blocks) { return false; } @@ -1307,7 +1321,6 @@ public class Gnonograms.Region { for (int i = 0; i < n_available_blocks; i++) { bl = available_blocks[i]; - if (!tags[first_start, bl]) { available_blocks[i] = -1; block_count --; @@ -1318,7 +1331,6 @@ public class Gnonograms.Region { for (int i = n_available_blocks - 1; i >= 0; i --) { bl = available_blocks[i]; - if (bl >= 0 && !tags[last_end, bl]) { available_blocks[i] = -1; block_count --; @@ -1334,7 +1346,6 @@ public class Gnonograms.Region { int[] candidates = new int[block_count]; int candidates_count = 0; int combined_length = 0; - for (int i = 0; i < n_available_blocks; i++) { if (available_blocks[i] < 0) { continue; @@ -1345,24 +1356,21 @@ public class Gnonograms.Region { } } - combined_length += (candidates_count - 1); //allow for gap of at least 1 between blocks + //allow for gap of at least 1 between blocks + combined_length += (candidates_count - 1); - // for unambiguous assignment all sub regions must be separated by more than + // for unambiguous assignment, all sub regions must be separated by more than // the combined length of the candidate blocks and gaps int overall_length = last_end - first_start + 1; - if (overall_length < combined_length) { return false; } //consecutive regions must be separated so one block cannot cover both //either by finished cell or by distance - for (int ar = 0; ar < region_count - 1; ar++) { - bool regions_are_separate = false; - + var regions_are_separate = false; for (int i = available_subregions[ar].end; i < available_subregions[ar + 1].start; i++) { - if (tags[i, is_finished_pointer]) { regions_are_separate = true; break; @@ -1375,7 +1383,6 @@ public class Gnonograms.Region { start = available_subregions[ar].start; end = available_subregions[ar + 1].end; length = end - start + 1; - if (length <= blocks[candidates[ar]] || length <= blocks[candidates[ar + 1]]) { return false; //too close } @@ -1408,7 +1415,6 @@ public class Gnonograms.Region { // increments/decrements idx until cell of required state // or end of range found. // returns idx of cell with status cs if found else limit - if (limit < -1 || limit > n_cells) { in_error = true; message = "limit < -1 || limit > n_cells"; @@ -1422,7 +1428,6 @@ public class Gnonograms.Region { } for (int i = idx; i != limit; i += direction) { - if (status[i] == cs) { return i; } @@ -1434,17 +1439,13 @@ public class Gnonograms.Region { private int count_consecutive_with_state_from (int cs, int idx, bool forwards) { // count how may consecutive cells of state cs starting at given // index idx (inclusive of starting cell) - int count = 0; - if (forwards && idx >= 0) { - while (idx < n_cells && status[idx] == cs) { count++; idx++; } } else if (!forwards && idx < n_cells) { - while (idx >= 0 && status[idx] == cs) { count++; idx--; @@ -1460,9 +1461,7 @@ public class Gnonograms.Region { private int count_contiguous_with_same_owner_from (int owner, int idx) { // count how may consecutive cells with owner possible starting // at given index idx? - int count = 0; - if (idx >= 0) { while (idx < n_cells && tags[idx, owner] && !tags[idx, is_finished_pointer]) { count++; @@ -1488,7 +1487,6 @@ public class Gnonograms.Region { int start = 0; int length = 0; int idx = 0; - //skip to start of first range; while (idx < n_cells && tags[idx, is_finished_pointer]) { idx++; @@ -1500,9 +1498,7 @@ public class Gnonograms.Region { ranges[range, 0] = start; ranges[range, 2] = 0; ranges[range, 3] = 0; - while (idx < n_cells && !tags[idx, is_finished_pointer]) { - if (!tags[idx, can_be_empty_pointer]) { ranges[range, 2]++; //FILLED } else { @@ -1531,11 +1527,8 @@ public class Gnonograms.Region { private bool match_clue () { //only called when region is completed. Checks whether number of blocks is correct - int count = 0, idx = 0, blk_ptr = 0, blk_counter = 0; - while (idx < n_cells) { - while (idx < n_cells && status[idx] == CellState.EMPTY) { idx++; } @@ -1572,12 +1565,10 @@ public class Gnonograms.Region { private int count_capped_ranges () { // determine location of capped ranges of filled cells (not marked complete) and store in ranges[, ] - int range = 0; int start = 0; int length = 0; int idx = 0; - while (idx < n_cells && status[idx] != CellState.FILLED) { idx++; //skip to beginning of first range } @@ -1588,21 +1579,19 @@ public class Gnonograms.Region { ranges[range, 0] = start; ranges[range, 2] = 0; //not used ranges[range, 3] = 0; //not used - while (idx < n_cells && status[idx] == CellState.FILLED) { idx++; length++; } if ((start == 0 || status[start - 1] == CellState.EMPTY) && - (idx == n_cells || status[idx] == CellState.EMPTY)) { //capped - + (idx == n_cells || status[idx] == CellState.EMPTY) + ) { //capped ranges[range, 1] = length; range++; } idx++; - while (idx < n_cells && status[idx] != CellState.FILLED) { idx++; //skip to beginning of next range } @@ -1613,9 +1602,7 @@ public class Gnonograms.Region { private int count_possible_owners_and_can_be_empty (int cell) { // how many possible owners? Does include can be empty tag! - int count = 0; - if (is_invalid_data (cell)) { in_error = true; message = "count_possible_owners_and_can_be_empty 1"; @@ -1641,9 +1628,7 @@ public class Gnonograms.Region { private int count_cell_state (int cs) { //how many times does state cs occur in range. - int count = 0; - for (int i = 0; i < n_cells; i++) { if (status[i] == cs) { count++; @@ -1655,9 +1640,7 @@ public class Gnonograms.Region { private int[] get_blocks_available () { //array of incomplete block indexes - int[] blocks = {}; - for (int i = 0; i < n_blocks; i++) { if (!completed_blocks[i]) { blocks += i; @@ -1670,19 +1653,15 @@ public class Gnonograms.Region { private int have_same_owner (int cell1, int cell2) { //checks if both the same single possible owner. //return owner if same owner else -1 - int count = 0; int owner = -1; bool tmp; - if (cell1 < 0 || cell1 >= n_cells || cell2 < 0 || cell2 >= n_cells) { in_error = true; message = "have_same_owner"; } else { - for (int i = 0; i < n_blocks; i++) { tmp = tags[cell1, i]; - if (count > 1 || (tmp != tags[cell2, i])) { owner = -1; break; @@ -1699,12 +1678,9 @@ public class Gnonograms.Region { private int get_sole_owner (int cell) { // if only one possible owner (if not empty) then return owner index // else return -1. - int count = 0; int owner = -1; - for (int i = 0; i < n_blocks; i++) { - if (tags[cell, i]) { owner = i; count++; @@ -1721,7 +1697,6 @@ public class Gnonograms.Region { private bool fix_block_in_range (int block, int start, int length) { // block must be limited to range var changed = false; - if (is_invalid_data (start, block, length)) { in_error = true; message = "fix_block_in_range"; @@ -1748,11 +1723,8 @@ public class Gnonograms.Region { private int find_largest_possible_block_for_cell (int cell) { // find the largest incomplete block possible for given cell - int maxsize = -1; - for (int i = 0; i < n_blocks; i++) { - if (!tags[cell, i]) { continue; // not possible } @@ -1769,9 +1741,7 @@ public class Gnonograms.Region { private int find_smallest_possible_block_for_cell (int cell) { // find the smallest incomplete block possible for given cell - int minsize = 9999; - for (int i = 0; i < n_blocks; i++) { if (!tags[cell, i]) { continue; // not possible @@ -1797,10 +1767,8 @@ public class Gnonograms.Region { //bi-directional forward = 1 backward = -1 //if reverse direction then equivalent forward range is used //only changes tags - int length = direction > 0 ? n_cells - start : start + 1; start = direction > 0 ? start : 0; - if (length > 0) { remove_block_from_range (block, start, length, 1); } @@ -1811,7 +1779,6 @@ public class Gnonograms.Region { //bi-directional forward = 1 backward = -1 //if reverse direction then equivalent forward range is used //only changes tags - if (direction < 0) { start = start - length + 1; } @@ -1820,7 +1787,6 @@ public class Gnonograms.Region { in_error = true; message = "remove_block_from_range"; } else { - for (int i = start; i < start + length; i++) { tags[i, block] = false; } @@ -1831,7 +1797,6 @@ public class Gnonograms.Region { //returns true - always changes a cell status if not in error bool changed = false; int length = blocks[block]; - if (direction < 0) { start = start - length + 1; } @@ -1850,7 +1815,6 @@ public class Gnonograms.Region { completed_blocks[block] = true; set_range_owner (block, start, length, true, false); - if (start > 0 && !tags[start - 1, is_finished_pointer]) { changed = true; set_cell_empty (start - 1); @@ -1868,7 +1832,6 @@ public class Gnonograms.Region { //taking into account minimum distance between blocks. // constrain the preceding blocks if this are at least two int l; - if (block > 1) { //at least third block l = 0; @@ -1881,7 +1844,6 @@ public class Gnonograms.Region { // constrain the following blocks if there are at least two if (block < n_blocks - 2) { l = 0; - for (int bl = block + 2; bl <= n_blocks - 1; bl++) { l = l + blocks[bl - 1] + 1; // length of exclusion zone for this block remove_block_from_range (bl, start + length + 1, l, 1); @@ -1893,14 +1855,12 @@ public class Gnonograms.Region { private bool set_range_owner (int owner, int start, int length, bool exclusive, bool can_be_empty) { bool changed = false; - if (is_invalid_data (start, owner, length)) { in_error = true; message = "set_range_owner 1"; return false; } else { int blocklength = blocks[owner]; - for (int cell = start; cell < start + length; cell++) { set_cell_owner (cell, owner, exclusive, can_be_empty); } @@ -1914,25 +1874,21 @@ public class Gnonograms.Region { } int bstart = int.min (start - 1, start + length - blocklength); - if (bstart >= 0) { remove_block_from_cell_to_end (owner, bstart - 1, -1); } int bend = int.max (start + length, start + blocklength); - if (bend < n_cells) { remove_block_from_cell_to_end (owner, bend, 1); } int earliestend = start + length; - for (int bl = n_blocks - 1; bl > owner; bl --) { //following blocks cannot be earlier remove_block_from_cell_to_end (bl, earliestend, -1); } int lateststart = start - 1; - for (int bl = 0; bl < owner; bl++) { //preceding blocks cannot be later remove_block_from_cell_to_end (bl, lateststart, 1); } @@ -1946,15 +1902,19 @@ public class Gnonograms.Region { //exclusive - cant be any other block here //can be empty - self evident bool changed = false; - if (is_invalid_data (cell, owner)) { - record_error ("set_cell_owner", - "invalid data %i, %i, %s, %s" - .printf (cell, owner, exclusive.to_string (), can_be_empty.to_string ())); - + record_error ( + "set_cell_owner", + "invalid data %i, %i, %s, %s".printf ( + cell, owner, exclusive.to_string (), can_be_empty.to_string () + ) + ); } else if (status[cell] == CellState.EMPTY) {// do nothing - not necessarily an error } else if (status[cell] == CellState.COMPLETED && tags[cell, owner] == false) { - record_error ("set_cell_owner", "contradiction cell " + cell.to_string () + " filled but cannot be owner"); + record_error ( + "set_cell_owner", + "contradiction cell " + cell.to_string () + " filled but cannot be owner" + ); } else { if (exclusive) { for (int i = 0; i < n_blocks; i++) { @@ -1982,7 +1942,6 @@ public class Gnonograms.Region { } else if (is_cell_filled (cell)) { record_error ("set_cell_empty", "cell " + cell.to_string () + " is filled"); } else { - for (int i = 0; i < n_blocks; i++) { tags[cell, i] = false; } @@ -2019,28 +1978,30 @@ public class Gnonograms.Region { private bool totals_changed () { //has number of filled or unknown cells changed? - bool changed = false; int _unknown = count_cell_state (CellState.UNKNOWN); int _filled = count_cell_state (CellState.FILLED); int _completed = count_cell_state (CellState.COMPLETED); - if (_unknown != this.unknown) { changed = true; this.unknown = _unknown; this.filled = _filled; - if (_filled + _completed > block_total) { - record_error ("totals changed", - ("too many filled cells filled %i, completed %i, block total %i") - .printf (_filled, _completed, block_total)); + record_error ( + "totals changed", + "too many filled cells filled %i, completed %i, block total %i".printf ( + _filled, _completed, block_total + ) + ); } else if (this.unknown == 0) { if (_filled + _completed < block_total) { - record_error ("totals changed", - ("too few filled cells filled %i, completed %i, block total %i") - .printf (_filled, _completed, block_total)); - + record_error ( + "totals changed", + "too few filled cells filled %i, completed %i, block total %i".printf ( + _filled, _completed, block_total + ) + ); } else if (match_clue ()) { this.is_completed = true; } else { @@ -2056,26 +2017,27 @@ public class Gnonograms.Region { private void get_status () { //transfers cell statuses from grid to internal range status array - grid.get_array (index, is_column, ref temp_status); - for (int i = 0; i < n_cells; i++) { - switch (temp_status[i]) { - case CellState.EMPTY : if (!tags[i, can_be_empty_pointer]) { - record_error ("get_status", "cell " + i.to_string () + " cannot be empty"); + record_error ( + "get_status", + "cell " + i.to_string () + " cannot be empty" + ); } else { status[i] = CellState.EMPTY; } break; - case CellState.FILLED : //dont overwrite COMPLETE status if (status[i] == CellState.EMPTY) { - record_error ("get_status", "cell " + i.to_string () + " cannot be filled"); + record_error ( + "get_status", + "cell " + i.to_string () + " cannot be filled" + ); } if (status[i] == CellState.UNKNOWN) { @@ -2083,7 +2045,6 @@ public class Gnonograms.Region { } break; - default: break; } @@ -2095,7 +2056,8 @@ public class Gnonograms.Region { private void put_status () { //use temp_status2 to ovoid overwriting original input - needed for debugging for (int i = 0; i < n_cells; i++) { - temp_status2[i] = (status[i] == CellState.COMPLETED ? CellState.FILLED : status[i]); + temp_status2[i] = status[i] == CellState.COMPLETED ? + CellState.FILLED : status[i]; } grid.set_array (index, is_column, temp_status2); @@ -2139,7 +2101,8 @@ public class Gnonograms.Region { } if (!tags[i, can_be_empty_pointer]) { //cannot be EMPTY - status[i] = (tags[i, is_finished_pointer] ? CellState.COMPLETED : CellState.FILLED); + status[i] = tags[i, is_finished_pointer] ? + CellState.COMPLETED : CellState.FILLED; continue; } @@ -2153,7 +2116,8 @@ public class Gnonograms.Region { private void record_error (string method, string errmessage) { in_error = true; - message = "%s Region %u Record error in %s : %s \n" - .printf (is_column ? "COL" : "ROW", index, method, errmessage); + message = "%s Region %u Record error in %s : %s \n".printf ( + is_column ? "COL" : "ROW", index, method, errmessage + ); } } diff --git a/libcore/Filereader.vala b/src/services/Filereader.vala similarity index 83% rename from libcore/Filereader.vala rename to src/services/Filereader.vala index 247a6f8..65a5b12 100644 --- a/libcore/Filereader.vala +++ b/src/services/Filereader.vala @@ -1,28 +1,14 @@ -/* Filereader.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Author: Jeremy Wootten < jeremwootten@gmail.com > + * Authored by: Jeremy Wootten */ - public class Gnonograms.Filereader : Object { public string err_msg = ""; public File? game_file { get; set; default = null;} - public GameState state { get; private set; default = GameState.UNDEFINED;} + public GameState state { get; private set; } public int rows { get; private set; default = 0;} public int cols { get; private set; default = 0;} @@ -33,6 +19,7 @@ public class Gnonograms.Filereader : Object { public string[] working { get; private set; } public string name { get; private set; default = "";} + public string author { get; private set; default = "";} public string date { get; private set; default = "";} public Difficulty difficulty { get; private set; default = Difficulty.UNDEFINED;} public string license { get; private set; default = "";} @@ -45,7 +32,6 @@ public class Gnonograms.Filereader : Object { public bool has_solution { get; private set; default = false;} public bool has_working { get; private set; default = false;} public bool has_state { get; private set; default = false;} - public bool is_readonly { get; private set; default = true;} public bool valid { get { @@ -53,11 +39,15 @@ public class Gnonograms.Filereader : Object { } } - public Filereader (Gtk.Window? parent, string? load_dir_path, File? game) throws GLib.IOError { - Object (game_file: game); - + public async void read ( + Gtk.Window? parent, + string? load_dir_path, + File? game + ) throws Error { if (game == null) { - game_file = get_load_game_file (parent, load_dir_path); + game_file = yield get_load_game_file (parent, load_dir_path); + } else { + game_file = game; } if (game_file == null) { @@ -73,23 +63,19 @@ public class Gnonograms.Filereader : Object { } parse_gnonogram_game_file (stream); - } - private File? get_load_game_file (Gtk.Window? parent, string? load_dir_path) { - string? path = Utils.get_open_save_path ( + private async File? get_load_game_file ( + Gtk.Window? parent, + string? load_dir_path + ) throws Error { + return yield Utils.get_open_save_file ( parent, _("Choose a puzzle"), false, load_dir_path, "" ); - - if (path == null || path == "") { - return null; - } else { - return File.new_for_path (path); - } } private void parse_gnonogram_game_file (DataInputStream stream) throws GLib.IOError { @@ -188,10 +174,6 @@ public class Gnonograms.Filereader : Object { in_error = !get_game_description (body); break; - case "LOC": - in_error = !get_readonly (body); - break; - case "ORI": in_error = !get_original_game_path (body); break; @@ -296,13 +278,16 @@ public class Gnonograms.Filereader : Object { private bool get_gnonogram_state (string? body) { /* Default to SOLVING state to avoid inadvertently showing solution */ - state = GameState.SOLVING; + has_state = false; if (body != null) { string[] s = Utils.remove_blank_lines (body.split ("\n")); if (s != null && s.length == 1) { + has_state = true; var state_string = s[0]; if (state_string.up ().contains ("SETTING")) { - state = GameState.SETTING; + state = SETTING; + } else { + state = SOLVING; } } } @@ -310,7 +295,7 @@ public class Gnonograms.Filereader : Object { return true; } - /** First four lines of description must be in order @name, @date, @score (difficulty or grade). + /** First four lines of description must be in order @name, @date, @score * Missing data must be represented by blank lines. **/ private bool get_game_description (string? body) { @@ -324,11 +309,15 @@ public class Gnonograms.Filereader : Object { } if (s.length >= 2) { - date = s[1]; + author = s[1]; } if (s.length >= 3) { - var grade = s[2].strip (); + date = s[2]; + } + + if (s.length >= 4) { + var grade = s[3].strip (); if (grade.length == 1 && grade[0].isdigit ()) { difficulty = (Difficulty)(int.parse (grade)); } else { @@ -339,22 +328,6 @@ public class Gnonograms.Filereader : Object { return true; } - private bool get_readonly (string? body) { - if (body == null) { - return true; /* Not mandatory */ - } - - string[] s = Utils.remove_blank_lines (body.split ("\n")); - bool result = true; - if (s.length >= 1) { - bool.try_parse (s[0].down (), out result); - } - - is_readonly = result; - - return true; - } - private bool get_original_game_path (string? body) { string result = ""; if (body != null) { diff --git a/src/services/Filewriter.vala b/src/services/Filewriter.vala new file mode 100644 index 0000000..a0c04ac --- /dev/null +++ b/src/services/Filewriter.vala @@ -0,0 +1,167 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.Filewriter : Object { + public DateTime date { get; construct; } + public Gtk.Window? parent { get; construct; } + + public uint rows { get; construct; } + public uint cols { get; construct; } + public string[] row_clues { get; construct; } + public string[] col_clues { get; construct; } + public string? game_path { get; set construct; } + public string? save_to_path { get; set construct; } + public string? save_dir_path { get; construct; } + + public My2DCellArray? solution { get; set; default = null;} + public string game_name { get; set; default = _(UNTITLED_NAME); } + public Difficulty difficulty { get; set; default = Difficulty.UNDEFINED;} + public string author { get; set; default = "Unknown";} + public string license { get; set; default = "";} + + private FileStream? stream; + + public Filewriter ( + Gtk.Window? parent, + Dimensions dimensions, + string[] row_clues, + string[] col_clues, + string? save_dir_path, + string? game_path, + string? save_to_path + ) { + + Object ( + parent: parent, + rows: dimensions.height, + cols: dimensions.width, + row_clues: row_clues, + col_clues: col_clues, + save_dir_path: save_dir_path, + game_path: game_path, + save_to_path: save_to_path + ); + } + + construct { + date = new DateTime.now_local (); + } + + /*** Writes minimum information required for valid game file ***/ + public async void write_game_file (SaveFlags flags) throws Error { + if (save_to_path == null || save_to_path.length <= 4) { + var save_to_file = yield Utils.get_open_save_file ( + parent, + _("Name and save this puzzle"), + true, + save_dir_path, + game_name + ); + + if (save_to_file != null) { + save_to_path = save_to_file.get_path (); + } + } + + if (save_to_path != null && + (save_to_path.length < 4 || + save_to_path[-4 : save_to_path.length] != Gnonograms.GAMEFILEEXTENSION)) { + + save_to_path = save_to_path + Gnonograms.GAMEFILEEXTENSION; + } + + if (save_to_path == null) { + throw new IOError.CANCELLED ("No save path selected"); + } + + var file = File.new_for_commandline_arg (save_to_path); + if (CONFIRM_OVERWRITE in flags && + file.query_exists ()) { + var overwrite = Utils.show_confirm_dialog ( + _("Overwrite %s").printf (save_to_path), + _("This action will destroy contents of that file"), + parent + ); + + if (!overwrite) { + throw new IOError.CANCELLED ("File exists"); + } + } + + /* @game_path is local path, not a uri */ + stream = FileStream.open (save_to_path, "w"); + if (stream == null) { + throw new IOError.FAILED ("Could not open filestream to %s".printf (save_to_path)); + } + + stream.printf ("[Description]\n"); + stream.printf ("%s\n", game_name); + stream.printf ("%s\n", author); + stream.printf ("%s\n", date.to_string ()); + stream.printf ("%u\n", difficulty); + + if (license == null || license.length > 0) { + stream.printf ("[License]\n"); + stream.printf ("%s\n", license); + } + + if (rows == 0 || cols == 0) { + throw new IOError.NOT_INITIALIZED ("No dimensions to save"); + } + + stream.printf ("[Dimensions]\n"); + stream.printf ("%u\n", rows); + stream.printf ("%u\n", cols); + + if (row_clues.length == 0 || col_clues.length == 0) { + throw new IOError.NOT_INITIALIZED ("No clues to save"); + } + + if (row_clues.length != rows || col_clues.length != cols) { + throw new IOError.NOT_INITIALIZED ("Clues do not match dimensions"); + } + + stream.printf ("[Row clues]\n"); + foreach (string s in row_clues) { + stream.printf ("%s\n", s); + } + + stream.printf ("[Column clues]\n"); + foreach (string s in col_clues) { + stream.printf ("%s\n", s); + } + + stream.flush (); + + if (solution != null) { + stream.printf ("[Solution grid]\n"); + stream.printf ("%s", solution.to_string ()); + } + } + + /*** Writes complete information to reload game state ***/ + public async void write_position_file ( + My2DCellArray working, + GameState state, + History history + ) throws Error { + yield write_game_file (SaveFlags.NONE); + + stream.printf ("[Working grid]\n"); + stream.printf (working.to_string ()); + stream.printf ("[State]\n"); + stream.printf (state.to_string () + "\n"); + stream.printf ("[Original path]\n"); + stream.printf (game_path.to_string () + "\n"); + + if (history != null) { + stream.printf ("[History]\n"); + stream.printf (history.to_string () + "\n"); + } + + stream.flush (); + } +} diff --git a/src/services/History.vala b/src/services/History.vala new file mode 100644 index 0000000..4256b22 --- /dev/null +++ b/src/services/History.vala @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.History : GLib.Object { + private class HistoryStack : Object { + public bool empty { + get { + return stack.is_empty; + } + } + + private Gee.Deque stack; + + construct { + stack = new Gee.LinkedList (); + } + + public void push_move (Move mv) requires (mv.is_valid ()) { + stack.offer_head (mv); + } + + public Move? peek_move () { + return stack.peek_head (); + } + + public Move? pop_move () { + return stack.poll_head (); + } + + public void clear () { + stack.clear (); + } + + public string to_string () { + var sb = new StringBuilder (""); + foreach (Move mv in stack) { /* iterates from head backwards */ + sb.prepend (mv.to_string () + ";"); + } + + sb.append ("\n"); + return sb.str; + } + } + + public bool can_go_back { + get { + return !back_stack.empty; + } + } + + public bool can_go_forward { + get { + return !forward_stack.empty; + } + } + + private HistoryStack back_stack; + private HistoryStack forward_stack; + + public signal void can_go_changed (bool forward, bool back); + + construct { + back_stack = new HistoryStack (); + forward_stack = new HistoryStack (); + } + + private void signal_can_go_changed () { + can_go_changed (!forward_stack.empty, !back_stack.empty ); + } + + public void clear_all () { + forward_stack.clear (); + back_stack.clear (); + signal_can_go_changed (); + } + + public void record_move (Cell? cell, CellState previous_state) { + if (cell == null) { + return; + } + + var new_move = new Gnonograms.Move.from_cell (cell, previous_state); + Move? last_move = back_stack.peek_move (); + if (new_move.equal (last_move)) { + return; + } + + forward_stack.clear (); + + back_stack.push_move (new_move); + signal_can_go_changed (); + } + + public Move pop_next_move () { + Move mv = forward_stack.pop_move (); + back_stack.push_move (mv); + signal_can_go_changed (); + return mv; + } + + public Move pop_previous_move () { + Move mv = back_stack.pop_move (); + /* Record copy otherwise it will be altered by next line*/ + forward_stack.push_move (mv.clone ()); + mv.cell.state = mv.previous_state; + signal_can_go_changed (); + return mv; + } + + public Move? get_current_move () { + return back_stack.peek_move (); + } + + public string to_string () { + return back_stack.to_string () + forward_stack.to_string (); + } + + public void from_string (string? s) { + clear_all (); + if (s == null) { + return; + } + + var stacks = Utils.remove_blank_lines (s.split ("\n")); + if (stacks != null) { + add_to_stack_from_string (stacks[0], true); + } + + if (stacks.length > 1) { + add_to_stack_from_string (stacks[1], false); + } + } + + private bool add_to_stack_from_string (string? s, bool back) { + if (s == null) { + return false; + } + + var moves_s = s.split (";"); + if (moves_s == null) { + return false; + } + + foreach (string move_s in moves_s) { + try { + var move = Move.from_string (move_s); + if (move != null) { + if (back) { + back_stack.push_move (move); + } else { + forward_stack.push_move (move); + } + } + } catch (Error e) { + warning ("Could not convert %s to Move. %s", move_s, e.message); + return false; + } + } + + signal_can_go_changed (); + return true; + } +} diff --git a/src/services/RandomGameGenerator.vala b/src/services/RandomGameGenerator.vala index bdcbae6..2609908 100644 --- a/src/services/RandomGameGenerator.vala +++ b/src/services/RandomGameGenerator.vala @@ -1,20 +1,8 @@ -/* SimpleRandomGameGenerator.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ public class Gnonograms.SimpleRandomGameGenerator : Object { public RandomPatternGenerator pattern_gen { get; construct; } diff --git a/src/services/RandomPatternGenerator.vala b/src/services/RandomPatternGenerator.vala index 337eb93..45471c2 100644 --- a/src/services/RandomPatternGenerator.vala +++ b/src/services/RandomPatternGenerator.vala @@ -1,20 +1,8 @@ -/* RandomPatternGenerator.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ public class Gnonograms.RandomPatternGenerator : Object { public Dimensions dimensions { get; construct; } @@ -106,11 +94,9 @@ public class Gnonograms.RandomPatternGenerator : Object { min_freedom = 4; break; case Difficulty.UNDEFINED: + case Difficulty.COMPUTER: /* May not be defined on creation */ break; - default: - critical ("unexpected grade %s", grade.to_string ()); - assert_not_reached (); } } diff --git a/src/services/ShortcutHelper.vala b/src/services/ShortcutHelper.vala new file mode 100644 index 0000000..d6a854f --- /dev/null +++ b/src/services/ShortcutHelper.vala @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + + public class Gnonograms.ShortcutHelper : Object { + private Gtk.ShortcutsWindow window; + + construct { + var builder = new Gtk.Builder.from_string (SHORTCUT_HELPER_UI, -1); + window = (Gtk.ShortcutsWindow) builder.get_object ("shortcuts-window"); + } + + public void show_window () { + window.present (); + } + } diff --git a/libcore/Solver.vala b/src/services/Solver.vala similarity index 92% rename from libcore/Solver.vala rename to src/services/Solver.vala index 093dab9..47839d6 100644 --- a/libcore/Solver.vala +++ b/src/services/Solver.vala @@ -1,22 +1,9 @@ -/* Solver.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ - public class Gnonograms.Solver : Object { public SolverState state { get; set; } public My2DCellArray grid { get; protected set; } // Shared with Regions which can update the contents @@ -87,7 +74,6 @@ assert (row_clues.length == rows && col_clues.length == cols); should_check_solution = solution_grid != null; - if (should_check_solution) { solution.copy (solution_grid); } @@ -112,10 +98,6 @@ return valid (); } - /** Initiate solving, specifying whether or not to use the advanced - * procedures. Also specify whether in debugging mode and whether to solve one step - * at a time (used for hinting if implemented). - **/ public async Difficulty solve_clues (string[] row_clues, string[] col_clues, My2DCellArray? start_grid = null, @@ -184,6 +166,7 @@ case Difficulty.MODERATE: case Difficulty.HARD: case Difficulty.CHALLENGING: + case Difficulty.UNDEFINED: break; case Difficulty.ADVANCED: @@ -204,8 +187,6 @@ human_only = false; break; - default: - assert_not_reached (); } } @@ -230,9 +211,13 @@ } #if WITH_DEBUGGING - public Gee.ArrayQueue debug (uint idx, bool is_column, string[] row_clues, - string[] col_clues, My2DCellArray working) { - + public Gee.ArrayQueue debug ( + uint idx, + bool is_column, + string[] row_clues, + string[] col_clues, + My2DCellArray working + ) { initialize (row_clues, col_clues, working, null); var moves = new Gee.ArrayQueue (); @@ -263,9 +248,12 @@ } #endif - public Gee.ArrayQueue hint (string[] row_clues, string[] col_clues, My2DCellArray working) { + public Gee.ArrayQueue hint ( + string[] row_clues, + string[] col_clues, + My2DCellArray working + ) { initialize (row_clues, col_clues, working, null); - bool changed = false; uint count = 0; var moves = new Gee.ArrayQueue (); @@ -280,15 +268,15 @@ var row = r.is_column ? i : r.index; var col = r.is_column ? r.index : i; Cell c = {row, col, r_state}; - moves.add (new Move (c, csa[i])); + moves.add (new Move.from_cell (c, csa[i])); changed = true; } } } while (!changed && count < 2 && - state != SolverState.ERROR) { /* May require two passes before a state changes */ - + state != SolverState.ERROR + ) { /* May require two passes before a state changes */ changed = false; count++; foreach (Region r in regions) { @@ -312,7 +300,7 @@ var row = r.is_column ? i : r.index; var col = r.is_column ? r.index : i; Cell c = {row, col, r_state}; - moves.add (new Move (c, csa[i])); + moves.add (new Move.from_cell (c, csa[i])); break; } } @@ -385,20 +373,19 @@ int empty = 0; int min_empty_cells = int.MAX; int changed_count = 0; - Cell best_guess = NULL_CELL; + Cell? best_guess = null; state = SolverState.UNDEFINED; while (state == SolverState.UNDEFINED) { changed_count++; - if (!guesser.next_guess ()) { state = SolverState.NO_SOLUTION; - if (best_guess.equal (NULL_CELL)) { // No improvement from last round + if (best_guess == null) { // No improvement from last round break; } else { grid.set_data_from_cell (best_guess); guesser = new Guesser (grid, false); - best_guess = NULL_CELL; + best_guess = null; changed_count = 0; if (!guesser.next_guess ()) { warning ("No next guess"); @@ -409,7 +396,6 @@ result = yield simple_solver (); initial_state = state; - if (initial_state == SolverState.NO_SOLUTION) { empty = solution.count_state (CellState.EMPTY); if (empty < min_empty_cells) { @@ -426,7 +412,6 @@ } contra = result; - /* Try opposite to check whether ambiguous or unique */ guesser.invert_previous_guess (); result = yield simple_solver (); @@ -480,7 +465,7 @@ result = yield simple_solver (); state = SolverState.AMBIGUOUS; } - } else if (initial_state == SolverState.ERROR) { // already checked for too may passes to contradiction. + } else if (initial_state == SolverState.ERROR) { guesser.initialize (); /* Continue from this position */ state = SolverState.UNDEFINED; @@ -539,7 +524,6 @@ /** Only call if simple solver used **/ private Difficulty passes_to_grade (uint passes) { Difficulty result; - if (passes == 0) { result = Difficulty.UNDEFINED; } else if (state == SolverState.ADVANCED) { @@ -670,7 +654,7 @@ c++; cdir = 0; rdir = -1; - r--; + r--; } else if (rdir == -1 && r <= turn) { //back across bottom lh edge reached r++; turn++; diff --git a/src/ui/shortcuthelper.ui.vala b/src/ui/shortcuthelper.ui.vala new file mode 100644 index 0000000..d63910a --- /dev/null +++ b/src/ui/shortcuthelper.ui.vala @@ -0,0 +1,160 @@ +namespace Gnonograms { + const string SHORTCUT_HELPER_UI = """ + + + 1 + + + Keyboard Shortcuts + 15 + + + Drawing + + + F + Paint FILLED + + + + + E + Paint EMPTY + + + + + X + Paint UNKNOWN + + + + + Left Right Up Down + Move cursor + + + + + + + Game + + + <Ctrl>N <Ctrl>3 + Generate new game + + + + + <Ctrl>H F9 + Hint + + + + + <Ctrl>R F5 + Restart current game + + + + + <Ctrl>Z + Undo last move + + + + + <Shift><Ctrl>Z + Redo last move + + + + + F7 + Check for and remove errors + + + + + <Ctrl>H F9 + Hint + + + + + <Alt>S + Solve by computer + + + + + + + Mode + + + <Ctrl>1 + Designing mode + + + + + <Ctrl>2 + Solving mode + + + + + + + General + + + Menu + Show App Menu + + + + + <Ctrl>P + Show Preferences Dialog + + + + + <Ctrl>K F1 + Show Keyboard Shortcuts + + + + + + + Files + + + <Ctrl>O + Load game from a .gno file + + + + + <Ctrl>S + Save game to a .gno file + + + + + <Ctrl>S + Save game to a diffent file + + + + + + + + +"""; +} diff --git a/src/widgets/CellPattern.vala b/src/widgets/CellPattern.vala new file mode 100644 index 0000000..80dd47e --- /dev/null +++ b/src/widgets/CellPattern.vala @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class CellPattern : GLib.Object { + public Cairo.Pattern pattern; + public double width { get; construct; } + public double height { get; construct; } + private double red; + private double green; + private double blue; + private double x0 = 0; + private double y0 = 0; + private Cairo.Matrix matrix; + + public CellPattern.cell (Gdk.RGBA color) { + red = color.red; + green = color.green; + blue = color.blue; + matrix = Cairo.Matrix.identity (); + + var granite_settings = Granite.Settings.get_default (); + set_pattern (granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK); + + granite_settings.notify["prefers-color-scheme"].connect (() => { + set_pattern (granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK); + }); + } + + public CellPattern.highlight (double wd, double ht) { + Object ( + width: wd, + height: ht + ); + } + + construct { + var r = double.min (width, height) / 2.0; + var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, (int)width , (int)height); + var context = new Cairo.Context (surface); + context.set_source_rgb (0.0, 0.0, 0.0); + context.rectangle (0, 0, width, height); + context.fill (); + context.arc (width / 2.0, height / 2.0, r - 2.0, 0, 2 * Math.PI); + context.set_source_rgba (1.0, 1.0, 1.0, 0.5); + context.set_operator (Cairo.Operator.SOURCE); + context.fill (); + + pattern = new Cairo.Pattern.for_surface (surface); + pattern.set_extend (Cairo.Extend.NONE); + matrix = Cairo.Matrix.identity (); + pattern.set_matrix (matrix); + } + + public void move_to (double x, double y) { + var xx = x - x0; + var yy = y - y0; + matrix.translate (-xx, -yy); + pattern.set_matrix (matrix); + x0 = x; + y0 = y; + } + + private void set_pattern (bool is_dark) { + pattern = new Cairo.Pattern.rgba ( + is_dark ? red / 2 : red, + is_dark ? green / 2 : green, + is_dark ? blue / 2 : blue, + 1.0 + ); + } +} diff --git a/src/widgets/Cellgrid.vala b/src/widgets/Cellgrid.vala new file mode 100644 index 0000000..10e54dc --- /dev/null +++ b/src/widgets/Cellgrid.vala @@ -0,0 +1,397 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ + +public enum Gnonograms.CellState { + UNKNOWN, + EMPTY, + FILLED, + COMPLETED, + INVALID +} + +public struct Gnonograms.Cell { + public uint row; + public uint col; + public CellState state; + + public bool same_coords (Cell c) { + return (this.row == c.row && this.col == c.col); + } + + public bool equal (Cell? b) { + return ( + b != null && + this.row == b.row && + this.col == b.col && + this.state == b.state + ); + + } + + public bool same_place (Cell? b) { + return ( + b != null && + this.row == b.row && + this.col == b.col + ); + + } + + public Cell inverse () { + Cell c = {row, col, CellState.UNKNOWN }; + + if (this.state == CellState.EMPTY) { + c.state = CellState.FILLED; + } else { + c.state = CellState.EMPTY; + } + + return c; + } + + public Cell clone () { + return { row, col, state }; + } + + public string to_string () { + return "Row %u, Col %u, State %s".printf (row, col, state.to_string ()); + } +} + +public class Gnonograms.CellGrid : Gtk.DrawingArea { + public signal void leave (); + + public Cell? current_cell { get; set; } + public Cell? previous_cell { get; set; } + public bool frozen { get; set; } + public bool draw_only { get; set; default = false;} + /* Could have more options for cell pattern*/ + private CellPatternType _cell_pattern_type; + public CellPatternType cell_pattern_type { + get { + return _cell_pattern_type; + } + + set { + switch (value) { + case CellPatternType.CELL: /* plain color fill */ + filled_cell_pattern = new CellPattern.cell (fill_color); + empty_cell_pattern = new CellPattern.cell (empty_color); + unknown_cell_pattern = new CellPattern.cell (unknown_color); + _cell_pattern_type = value; + + break; + default: + /* Refresh colors of existing pattern */ + if (_cell_pattern_type != CellPatternType.UNDEFINED) { + cell_pattern_type = _cell_pattern_type; + } + + break; + } + } + } + + private const double MAJOR_GRID_LINE_WIDTH = 3.0; + private const double MINOR_GRID_LINE_WIDTH = 1.0; + private Gdk.RGBA[, ] colors; + + public double cell_width { get; private set; } /* Width and Height of cell including frame */ + public double cell_height { get; private set; }/* Width and Height of cell including frame */ + private bool dirty = false; /* Whether a redraw is needed */ + + private uint rows = 5; + private uint cols = 5; + private Gdk.RGBA grid_color; + private Gdk.RGBA fill_color; + private Gdk.RGBA empty_color; + private Gdk.RGBA unknown_color; + + private CellPattern filled_cell_pattern; + private CellPattern empty_cell_pattern; + private CellPattern unknown_cell_pattern; + private CellPattern highlight_pattern; + + private My2DCellArray? array { + get { + return model.display_data; + } + } + + private Controller controller = Controller.get_default (); + private Model model = Model.get_default (); + + construct { + hexpand = true; + vexpand = true; + current_cell = null; + colors = new Gdk.RGBA[2, 3]; + grid_color.parse (Gnonograms.GRID_COLOR); + cell_pattern_type = CellPatternType.CELL; + set_colors (); + + var motion_controller = new Gtk.EventControllerMotion (); + add_controller (motion_controller); + motion_controller.motion.connect (on_pointer_moved); + motion_controller.leave.connect (on_leave_notify); + + set_draw_func (draw_func); + + notify["current-cell"].connect (() => { + queue_draw (); + }); + + controller.notify["game-state"].connect (on_game_state_changed); + controller.notify["dimensions"].connect (on_dimensions_changed); + + model.changed.connect (() => { + if (!dirty) { + dirty = true; + queue_draw (); + } + }); + + settings.changed["filled-color"].connect (set_colors); + settings.changed["empty-color"].connect (set_colors); + } + + public void on_dimensions_changed () { + this.rows = controller.rows; + this.cols = controller.columns; + queue_allocate (); + } + + public void set_colors () { + // Ensure settings have updated + Idle.add (() => { + var setting = (int) GameState.SETTING; + colors[setting, (int) CellState.UNKNOWN].parse (Gnonograms.UNKNOWN_COLOR); + colors[setting, (int) CellState.EMPTY].parse (Gnonograms.SETTING_EMPTY_COLOR); + colors[setting, (int) CellState.FILLED].parse (Gnonograms.SETTING_FILLED_COLOR); + setting = (int) GameState.SOLVING; + colors[setting, (int) CellState.UNKNOWN].parse (Gnonograms.UNKNOWN_COLOR); + colors[setting, (int) CellState.EMPTY].parse (settings.get_string ("empty-color")); + colors[setting, (int) CellState.FILLED].parse (settings.get_string ("filled-color")); + update_colors (controller.game_state); + queue_draw (); + return Source.REMOVE; + }); + } + + private void on_game_state_changed () { + update_colors (controller.game_state); + } + + private void update_colors (GameState gs) { + unknown_color = colors[(int)gs, (int)CellState.UNKNOWN]; + fill_color = colors[(int)gs, (int)CellState.FILLED]; + empty_color = colors[(int)gs, (int)CellState.EMPTY]; + cell_pattern_type = CellPatternType.UNDEFINED; /* Causes refresh of existing pattern */ + } + + public override void size_allocate (int w, int h, int bl) { + var r = (double) rows; + var c = (double) cols; + // Need to allow window to be shrunk and create bottom/end margins + var dw = (double) w - c - 12; + var dh = (double) h - r - 12; + if (r == 0 || c == 0) { + return; + } + var width_for_height = dh * c / r; + var height_for_width = dw * r / c; + //Calculate content dimensions, optimise fit in available space, keeping square cells + double height, width; + if (width_for_height > dw) { + width = dw; + height = height_for_width; + } else if (height_for_width > dh) { + height = dh; + width = width_for_height; + } else { + height = dh; + width = dw; + } + + // Cell width and height should be the same but leave separate for now. + cell_width = width / c; + cell_height = height / r; + + content_width = (int) (width + 0.99); + content_height = (int) (height + 0.99); + + /* Cause refresh of existing pattern */ + highlight_pattern = new CellPattern.highlight (cell_width, cell_height); + } + + private void draw_func ( + Gtk.DrawingArea drawing_area, + Cairo.Context cr, + int x, + int y + ) { + + dirty = false; + if (array != null) { + /* Note, even tho' array holds CellStates, its iterator returns Cells */ + foreach (Cell? c in array) { + bool highlight = c.same_place (current_cell); + draw_cell (cr, c, highlight); + } + } + + draw_grid (cr); + } + + private double previous_pointer_x = 0.0; + private double previous_pointer_y = 0.0; + private void on_pointer_moved (double x, double y) { + if (draw_only || x < 0 || y < 0) { + return; + } + + // Need to ignore spurious "movements" in Gtk4 + if (previous_pointer_x == x && previous_pointer_y == y) { + return; + } else { + previous_pointer_x = x; + previous_pointer_y = y; + } + /* Calculate which cell the pointer is over */ + uint r = ((uint)((y) / cell_height)); + uint c = ((uint)(x / cell_width)); + /* Construct cell beneath pointer */ + Cell cell = {r, c, array.get_data_from_rc (r, c)}; + if (!cell.equal (current_cell)) { + if (current_cell == null) { + previous_cell = null; + } else { + previous_cell = current_cell.clone (); + } + current_cell = cell.clone (); + } + + return; + } + + private void draw_grid (Cairo.Context cr) { + Gdk.cairo_set_source_rgba (cr, grid_color); + cr.set_antialias (Cairo.Antialias.NONE); + cr.set_line_width (MINOR_GRID_LINE_WIDTH); + + var r = rows; + var c = cols; + var w = cell_width; + var h = cell_height; + // Draw minor grid lines + var x2 = w * c; + var y2 = h * r; + // Draw horizontal lines + for (int cell = 0; cell < r; cell++) { + var y1 = cell * h; + cr.move_to (0, y1); + cr.line_to (x2, y1); + cr.stroke (); + } + + // Draw vertical lines + for (int cell = 0; cell < c; cell++) { + var x1 = cell * w; + cr.move_to (x1, 0); + cr.line_to (x1, y2); + cr.stroke (); + } + + // Draw inner major grid lines + cr.set_line_width (MAJOR_GRID_LINE_WIDTH); + // Draw horizontal lines + for (int cell = 5; cell < r; cell += 5) { + var y1 = cell * h; + cr.move_to (0, y1); + cr.line_to (x2, y1); + cr.stroke (); + } + + // Draw vertical lines + for (int cell = 5; cell < c; cell += 5) { + var x1 = cell * w; + cr.move_to (x1, 0); + cr.line_to (x1, y2); + cr.stroke (); + } + + // Draw frame + cr.set_line_width (MINOR_GRID_LINE_WIDTH); + var y1 = MINOR_GRID_LINE_WIDTH; + var x1 = MINOR_GRID_LINE_WIDTH; + cr.move_to (x1, y1); + cr.line_to (x2 - x1, y1); + cr.stroke (); + + cr.move_to (x2 - x1, y1); + cr.line_to (x2 - x1, y2 - y1); + cr.stroke (); + + cr.move_to (x2 - x1, y2 - y1); + cr.line_to (x1, y2 - y1); + cr.stroke (); + + cr.move_to (x1, y2 - y1); + cr.line_to (x1, y1); + cr.stroke (); + } + + private void draw_cell (Cairo.Context cr, Cell cell, bool highlight = false, bool mark = false) { + if (frozen) { + return; + } + + double x = cell.col * cell_width; + double y = cell.row * cell_height; + CellPattern cell_pattern; + switch (cell.state) { + case CellState.EMPTY: + cell_pattern = empty_cell_pattern; + break; + + case CellState.FILLED: + cell_pattern = filled_cell_pattern; + break; + + default : + cell_pattern = unknown_cell_pattern; + break; + } + + cr.save (); + cell_pattern.move_to (x, y); /* Not needed for plain fill, but may use a pattern later */ + cr.set_line_width (0.0); + cr.rectangle (x, y, cell_width, cell_height); + cr.set_source (cell_pattern.pattern); + cr.fill (); + cr.restore (); + + if (highlight && !draw_only) { + cr.save (); + /* Ensure highlight centred and slightly overlapping grid */ + highlight_pattern.move_to (x, y); + cr.rectangle (x, y, cell_width, cell_height); + cr.clip (); + cr.set_source (highlight_pattern.pattern); + cr.set_operator (Cairo.Operator.OVER); + cr.paint (); + cr.restore (); + } + } + + private void on_leave_notify () { + previous_cell = null; + current_cell = null; + leave (); + return; + } +} diff --git a/libcore/widgets/Label.vala b/src/widgets/Clue.vala similarity index 65% rename from libcore/widgets/Label.vala rename to src/widgets/Clue.vala index 7d5cd9e..b939eba 100644 --- a/libcore/widgets/Label.vala +++ b/src/widgets/Clue.vala @@ -1,56 +1,21 @@ -/* Label.vala - * Copyright (C) 2010-2021 Jeremy Wootten +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten * - 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 . - * - * Author: Jeremy Wootten + * Authored by: Jeremy Wootten */ +class Gnonograms.Clue : Object { + public Gtk.Label label { get; construct; } + public unowned ClueBox cluebox { get; construct; } -class Gnonograms.Clue : Gtk.Label { - private uint _n_cells; - public uint n_cells { - set { - _n_cells = value; - update_tooltip (); - } - - private get { - return _n_cells; - } - } - - private int _fontsize; - private int _cell_size; - public int cell_size { + private string _text; /* text of clue in horizontal form */ + public string text { get { - return _cell_size; - } - set { - _cell_size = value; - _fontsize = (int)((double)value * 0.4); - update_markup (); - } - } - - private string _clue; /* text of clue in horizontal form */ - public string clue { - get { - return _clue; + return _text; } set { - _clue = value; + _text = value; clue_blocks = Utils.block_struct_array_from_clue (value); update_markup (); } @@ -58,52 +23,50 @@ class Gnonograms.Clue : Gtk.Label { public bool vertical_text { get; construct; } - private Gee.List clue_blocks; - private Gee.List grid_blocks; + private Gee.List clue_blocks; // List of blocks based on clue - public Clue (bool _vertical_text) { + public Clue (bool _vertical_text, ClueBox cluebox) { Object ( vertical_text: _vertical_text, - xalign: _vertical_text ? (float)0.5 : (float)1.0, - yalign: _vertical_text ? (float)1.0 : (float)0.5, - clue: "0", - has_tooltip: true, - use_markup: true, - margin: 0, - expand: true + cluebox: cluebox ); } construct { - realize.connect_after (() => { - update_markup (); - }); + label = new Gtk.Label ("") { + xalign = _vertical_text ? (float)0.5 : (float)1.0, + yalign = vertical_text ? (float)1.0 : (float)0.5, + has_tooltip = true, + use_markup = true, + }; + + text = "0"; + + label.realize.connect_after (update_markup); + cluebox.notify["cell-size"].connect (update_markup); } public void highlight (bool is_highlight) { if (is_highlight) { - get_style_context ().add_class (Granite.STYLE_CLASS_ACCENT); + label.add_css_class (Granite.STYLE_CLASS_ACCENT); } else { - get_style_context ().remove_class (Granite.STYLE_CLASS_ACCENT); + label.remove_css_class (Granite.STYLE_CLASS_ACCENT); } } public void clear_formatting () { - var sc = get_style_context (); - sc.remove_class ("warn"); - sc.remove_class ("dim"); + label.remove_css_class ("warn"); + label.remove_css_class ("dim"); } - public void update_complete (Gee.List _grid_blocks) { - grid_blocks = _grid_blocks; + public void update_complete (Gee.List grid_blocks) { foreach (Block block in clue_blocks) { block.is_complete = false; block.is_error = false; } - var sc = get_style_context (); - sc.remove_class ("warn"); - sc.remove_class ("dim"); + label.remove_css_class ("warn"); + label.remove_css_class ("dim"); uint complete = 0; uint errors = 0; @@ -149,12 +112,12 @@ class Gnonograms.Clue : Gtk.Label { } if (errors > 0) { - sc.add_class ("warn"); + label.add_css_class ("warn"); } if (complete == clue_blocks.size && errors == 0 && grid_null == 0) { update_markup (); - sc.add_class ("dim"); + label.add_css_class ("dim"); return; } @@ -214,25 +177,25 @@ class Gnonograms.Clue : Gtk.Label { } } } - } else if (clue != "0") { /* Zero grid blocks should only occur if cellstates all "empty" */ + } else if (text != "0") { /* Zero grid blocks should only occur if cellstates all "empty" */ errors++; } if (errors > 0) { - sc.add_class ("warn"); + label.add_css_class ("warn"); } update_markup (); } private void update_markup () { - set_markup ("".printf (_fontsize) + get_markup () + ""); + label.set_markup ("".printf (cluebox.font_desc.to_string ()) + get_markup () + ""); update_tooltip (); } private void update_tooltip () { - set_tooltip_markup ("".printf (_fontsize) + - _("Freedom = %u").printf (n_cells - Utils.blockextent_from_clue (_clue)) + + label.set_tooltip_markup ("".printf (cluebox.font_desc.to_string ()) + + _("Freedom = %u").printf (cluebox.n_cells - Utils.blockextent_from_clue (_text)) + "" ); } @@ -241,7 +204,7 @@ class Gnonograms.Clue : Gtk.Label { string attrib = ""; string weight = "bold"; string strikethrough = "false"; - bool warn = get_style_context ().has_class ("warn"); + bool warn = label.has_css_class ("warn"); StringBuilder sb = new StringBuilder (""); foreach (Block clue_block in clue_blocks) { @@ -257,7 +220,12 @@ class Gnonograms.Clue : Gtk.Label { attrib = "".printf (weight, strikethrough); sb.append (attrib); - sb.append (clue_block.length.to_string ()); + if (vertical_text) { + sb.append (" " + clue_block.length.to_string () + " "); + } else { + sb.append (clue_block.length.to_string ()); + } + sb.append (""); if (vertical_text) { sb.append ("\n"); diff --git a/src/widgets/Cluebox.vala b/src/widgets/Cluebox.vala new file mode 100644 index 0000000..cb967dc --- /dev/null +++ b/src/widgets/Cluebox.vala @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: 2010-2024 Jeremy Wootten + * + * Authored by: Jeremy Wootten + */ +public class Gnonograms.ClueBox : Gtk.Widget { + static construct { + set_layout_manager_type (typeof (Gtk.BoxLayout)); + } + + const int PIX_TO_PANGO_FONT = 1024 / 2; + + public bool holds_column_clues { get; construct; } + public uint n_cells { get; set; default = 0; }// The number of cells each clue addresses, monitored by clues + public double cell_size { get; set; } + public Pango.FontDescription font_desc { get; set; } + + private Gee.ArrayList clues; + private Controller controller = Controller.get_default (); + + public ClueBox (bool _holds_column_clues) { + Object ( + holds_column_clues: _holds_column_clues + ); + } + + construct { + var orientation = holds_column_clues ? Gtk.Orientation.HORIZONTAL : Gtk.Orientation.VERTICAL; + var layout = new Gtk.BoxLayout (orientation) { + homogeneous = false, + spacing = 0 + }; + set_layout_manager (layout); + + margin_bottom = holds_column_clues ? 0 : 6; + margin_end = holds_column_clues ? 6 : 0; + clues = new Gee.ArrayList (); + font_desc = Pango.FontDescription.from_string ("Arial 10"); + var mode = holds_column_clues ? Gtk.SizeGroupMode.HORIZONTAL : Gtk.SizeGroupMode.VERTICAL; + + if (holds_column_clues) { + hexpand = false; + } else { + vexpand = false; + } + + notify["cell-size"].connect (update_size_request); + controller.notify["dimensions"].connect (on_dimensions_changed); + } + + private void update_size_request () { + font_desc.set_absolute_size (cell_size * PIX_TO_PANGO_FONT); + var index = 0.0; + var size = (int) cell_size; + var diff = cell_size - (double) size; + var shortfall = 0.0; + // Assign label widths to match grid lines as closely as possible. + // As the cell dimensions are non-integral we have to vary the (integral) label widths + var box_size = 0; + foreach (Clue clue in clues) { + var makeup = 0; + if (shortfall >= 1.0) { + makeup = 1; + shortfall-= 1.0; + } + + var label = clue.label; + if (holds_column_clues) { + label.width_request = size + makeup; + box_size += label.width_request; + } else { + label.height_request = size + makeup; + box_size += label.height_request; + } + + index++; + shortfall += diff; + } + + if (holds_column_clues) { + set_size_request (box_size, (int) (cell_size / 2.0 * (n_cells / 3 + 2))); + } else { + set_size_request ((int) (cell_size / 2.0 * (n_cells / 3 + 2)), box_size); + } + } + + public void on_dimensions_changed () { + var rows = controller.rows; + var cols = controller.columns; + + var new_n_clues = holds_column_clues ? cols : rows; + var new_n_cells = holds_column_clues ? rows : cols; + + if (n_cells != new_n_cells) { + n_cells = new_n_cells; + } + + if (clues.size != new_n_clues) { + foreach (var clue in clues) { + clue.label.unparent (); + clue.label.destroy (); + } + + clues.clear (); + + for (int index = 0; index < new_n_clues; index++) { + var clue = new Clue (holds_column_clues, this); + clues.add (clue); + clue.label.set_parent (this); + } + } else { + foreach (var clue in clues) { + clue.text = "0"; + } + } + + update_size_request (); + } + + public string[] get_clue_texts () { + string[] clue_texts = {}; + foreach (var clue in clues) { + clue_texts += clue.text; + } + + return clue_texts; + } + + public void highlight (uint index, bool is_highlight) { + if (index < clues.size) { + clues[(int)index].highlight (is_highlight); + } + } + + public void unhighlight_all () { + foreach (var clue in clues) { + clue.highlight (false); + } + } + + public void update_clue_text (uint index, string? text) { + if (index < clues.size) { + clues[(int)index].text = text ?? _(BLANKLABELTEXT); + } + } + + public void clear_formatting (uint index) { + if (index < clues.size) { + clues[(int)index].clear_formatting (); + } + } + + public void update_clue_complete (uint index, Gee.List grid_blocks) { + if (index < clues.size) { + clues[(int)index].update_complete (grid_blocks); + } + } +}