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
+
+
+
+
+
+
+
@@ -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);
+ }
+ }
+}